Compare commits
942
Commits
30b8cf7db9
...
v2.5.0
@@ -0,0 +1,65 @@
|
||||
{
|
||||
"permissions": {
|
||||
"allow": [
|
||||
"Skill(pql)",
|
||||
"Skill(clide)",
|
||||
"Skill(whats-next)",
|
||||
"Skill(git-commit)",
|
||||
"Bash(git add *)",
|
||||
"Bash(git commit *)",
|
||||
"Bash(git status *)",
|
||||
"Bash(git diff *)",
|
||||
"Bash(git log *)",
|
||||
"Bash(git show *)",
|
||||
"Bash(git branch *)",
|
||||
"Bash(git checkout *)",
|
||||
"Bash(git switch *)",
|
||||
"Bash(git merge *)",
|
||||
"Bash(git rebase *)",
|
||||
"Bash(git stash *)",
|
||||
"Bash(git fetch *)",
|
||||
"Bash(git pull *)",
|
||||
"Bash(git push *)",
|
||||
"Bash(git tag *)",
|
||||
"Bash(git remote *)",
|
||||
"Bash(git rev-parse *)",
|
||||
"Bash(git symbolic-ref *)",
|
||||
"Bash(git ls-remote *)",
|
||||
"Bash(git config *)",
|
||||
"Bash(git blame *)",
|
||||
"Bash(git shortlog *)",
|
||||
"Bash(git cherry-pick *)",
|
||||
"Bash(dart *)",
|
||||
"Bash(flutter *)",
|
||||
"Bash(make *)",
|
||||
"Bash(pql)",
|
||||
"Bash(pql *)",
|
||||
"Bash(clide)",
|
||||
"Bash(clide *)",
|
||||
"Bash(awk *)"
|
||||
],
|
||||
"deny": [
|
||||
"Bash(rm -rf /*)",
|
||||
"Bash(rm -rf ~*)",
|
||||
"Bash(rm -rf $HOME*)",
|
||||
"Bash(sudo rm *)",
|
||||
"Bash(sudo chmod *)",
|
||||
"Bash(sudo chown *)",
|
||||
"Bash(mkfs*)",
|
||||
"Bash(mkfs.*)",
|
||||
"Bash(dd if=*)",
|
||||
"Bash(git push --force*)",
|
||||
"Bash(git push -f*)",
|
||||
"Bash(git push --force-with-lease*)",
|
||||
"Bash(git reset --hard*)",
|
||||
"Bash(git clean -fd*)",
|
||||
"Bash(git clean -fdx*)",
|
||||
"Bash(git clean -ffd*)",
|
||||
"Bash(git branch -D *)",
|
||||
"Bash(git checkout -- *)",
|
||||
"Bash(git restore .*)",
|
||||
"Bash(chmod -R 777 *)",
|
||||
"Bash(chmod 777 *)"
|
||||
]
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
# Clide Claude-Code skills
|
||||
|
||||
Skills are reusable instruction packs Claude Code loads on demand. Each one
|
||||
lives in its own directory with a `SKILL.md` (frontmatter + body) and any
|
||||
helper scripts. This index is for humans skimming what's available; Claude
|
||||
discovers skills automatically from the directory structure.
|
||||
|
||||
| Skill | Purpose |
|
||||
|---|---|
|
||||
| [`d2-diagram`](d2-diagram/SKILL.md) | Generate technical diagrams from `.d2` source with the d2 CLI; renders to PNG. |
|
||||
| [`frame0-wireframe`](frame0-wireframe/SKILL.md) | Author UI wireframes as local JSON and sync to the Frame0 desktop app for rendering + export. |
|
||||
| [`git-commit`](git-commit/SKILL.md) | Commit conventions for this repo — message style, CHANGELOG discipline (40/60 word cap), attribution trailer, safety rules. |
|
||||
| [`pql`](pql/SKILL.md) | Query and plan against the markdown vault via the `pql` CLI (decisions, tickets, structural queries). |
|
||||
| [`skill-create`](skill-create/SKILL.md) | Guidance for creating new skills — SKILL.md structure, bundling scripts, packaging. |
|
||||
| [`testmode`](testmode/SKILL.md) | Run and interpret the `ClideTestApp` platform integration harness; smoke-test after toolchain / IPC / theme / native changes. |
|
||||
| [`ui-design`](ui-design/SKILL.md) | Visual design guide — surface tokens, control geometry, Phosphor icons. |
|
||||
| [`whats-next`](whats-next/SKILL.md) | Dependency-driven batch selection against the pql backlog. Walks the initiative/epic tree, filters unblocked work, refines, optionally activates. |
|
||||
|
||||
## Adding a skill
|
||||
|
||||
Use the `skill-create` skill (or follow its SKILL.md by hand). Add a row to
|
||||
the table above so the inventory stays accurate; the index is otherwise just
|
||||
a directory listing.
|
||||
@@ -0,0 +1,5 @@
|
||||
{
|
||||
"version": "1.5.0",
|
||||
"hash": "sha256:8090b99552c828ce019aa3740d8a0acb4768a443704b4e935efa23b32e91128d",
|
||||
"installed_at": "2026-06-02T08:46:25Z"
|
||||
}
|
||||
@@ -0,0 +1,229 @@
|
||||
---
|
||||
name: clean-house
|
||||
description: >
|
||||
Run a documentation-discipline pass over a DQR (Decisions / Questions /
|
||||
Rejected) markdown system. Audits decisions/, surfaces drift (broken
|
||||
anchor links, missing Q→D backlinks, sunset-shaped phrases without
|
||||
linked tickets, files over the split threshold, dead cross-references),
|
||||
batches findings by category, and uses AskUserQuestion to apply fixes
|
||||
interactively. Trigger this skill whenever the user asks to clean,
|
||||
tidy, audit, sweep, garden, or review the state of decisions/, the
|
||||
DQR system, the questions index, or the records under decisions/.
|
||||
Also trigger before a release, after a session that touched many
|
||||
records, or whenever the user mentions documentation drift, stale
|
||||
plans, or "house" / "clean-house" / "house cleaning". Imperative —
|
||||
apply fixes, not just report.
|
||||
---
|
||||
|
||||
# clean-house
|
||||
|
||||
A periodic gardening pass over `decisions/`. The DQR system is
|
||||
markdown-as-source-of-truth; `.pql/pql.db` is a derived index. This skill
|
||||
operates on the markdown files, batches drift by category, and uses
|
||||
`AskUserQuestion` to decide what to apply.
|
||||
|
||||
The skill is **imperative**. It really cleans. The verb-noun match is
|
||||
load-bearing — a `clean-house` that only produced a report would be
|
||||
misnamed. Mechanical fixes get a single batched approval, judgment-call
|
||||
findings get individual prompts, and a "stop asking" escape hatch is
|
||||
always present.
|
||||
|
||||
You — Claude — are the runtime. Read this file, read
|
||||
`references/rules.md`, follow the procedure below, call `pql` and `Edit`
|
||||
to do the work, call `AskUserQuestion` for the prompts.
|
||||
|
||||
## When to run
|
||||
|
||||
- Before a release.
|
||||
- After a session that touched many D / Q / R records.
|
||||
- When something feels stale and a sweep is wanted.
|
||||
- When `pql decisions validate` is green but `decisions/` still feels off.
|
||||
|
||||
This skill is **not** a replacement for `pql decisions validate`.
|
||||
Validate is fast, hot, and gates pre-push. clean-house is slow, cold,
|
||||
on-demand. Escalation path: rules that fire often here are candidates
|
||||
for promotion to `pql decisions validate`; rules that almost never fire
|
||||
stay here where false positives don't slow anyone down.
|
||||
|
||||
## Procedure
|
||||
|
||||
1. **Gate on validate.** Run `pql decisions validate`. If it exits
|
||||
non-zero, stop and tell the user to fix validator findings first.
|
||||
Do not proceed.
|
||||
|
||||
2. **Load the rule catalog.** Read `references/rules.md`. Each rule
|
||||
names its detection, fix, and finding-id format.
|
||||
|
||||
3. **Probe conventions.** Read `decisions/.clean-house.yaml` if it
|
||||
exists; otherwise infer from the project. Conventions to resolve:
|
||||
- `heading_level`: 2 or 3 — the depth records use (`## D-N` vs
|
||||
`### D-N`). Probe by sampling the first 3 records' file
|
||||
locations and looking for the first matching ATX heading.
|
||||
- `backlink_phrasing`: the project's Q→D backlink phrase (default
|
||||
`Resolved → D-N`; some projects use `Resolved as D-N` or
|
||||
`→ D-N`).
|
||||
- `file_threshold`: default 350 (RULE-FILE-OVER-THRESHOLD).
|
||||
- `stale_open_q_days`: default 60 (RULE-STALE-OPEN-Q).
|
||||
|
||||
Pass the resolved conventions into each rule's detection. Cache
|
||||
the probe result for the run.
|
||||
|
||||
`decisions/.clean-house.yaml` shape (all keys optional):
|
||||
```yaml
|
||||
heading_level: 3
|
||||
backlink_phrasing: "Resolved → "
|
||||
file_threshold: 500
|
||||
stale_open_q_days: 90
|
||||
exclude_paths: ["legacy/", "drafts/"]
|
||||
```
|
||||
|
||||
4. **Sync the index.** Run `pql decisions sync` so the DB reflects
|
||||
the markdown.
|
||||
|
||||
5. **Walk decisions/.** Use `pql decisions list` to enumerate
|
||||
records, plus `pql decisions read <id>` per record when a rule
|
||||
needs the body. For file-level rules (size, sort), read the
|
||||
markdown files directly. **Filter out** any record whose
|
||||
`file_path` begins with `legacy/` (or any prefix in
|
||||
`exclude_paths` from the probe).
|
||||
|
||||
6. **Run each rule's detection.** Collect findings. Tag each with
|
||||
its rule ID, finding ID (per the rule's format), category
|
||||
(`mechanical` | `judgment`), and the record(s)/file(s) involved.
|
||||
|
||||
7. **Group findings by rule** and prompt. Honor any "skipped 3+
|
||||
times" findings from the skip ledger by escalating their prompt
|
||||
("skipped 3x — promote to manual decision, downgrade to
|
||||
summary-only, or keep asking?"):
|
||||
|
||||
- **Mechanical rules** — one prompt per rule, batched. Show the
|
||||
count and offer: apply all / show diff first / skip this rule /
|
||||
**stop asking** (global — see below).
|
||||
- **Judgment rules** — one prompt per finding. Options match the
|
||||
rule's `Fix` section, plus the same global **stop asking**.
|
||||
|
||||
**"Stop asking" semantics (global).** When the user picks "stop
|
||||
asking" on any prompt, immediately halt all further prompts in
|
||||
the run — mechanical batches not yet asked AND remaining
|
||||
judgment findings. Move all unprompted findings to the skip
|
||||
ledger marked `deferred-stop-asking`. Skip to step 9.
|
||||
|
||||
**"Show diff first" loop.** When the user picks "show diff
|
||||
first" on a mechanical batch, emit a unified diff of the staged
|
||||
edits (one block per file), then re-prompt the same question
|
||||
with the same options minus "show diff first" — preventing
|
||||
infinite loops while still letting the user inspect before
|
||||
committing.
|
||||
|
||||
8. **Apply approvals.** For mechanical rules use `Edit` to mutate
|
||||
the markdown directly. For judgment rules, follow the option the
|
||||
user picked (file a ticket via `pql ticket new`, mark superseded
|
||||
by editing the record, etc.).
|
||||
|
||||
9. **Update the skip ledger and history.** Both files live at
|
||||
`decisions/.clean-house-state.md`, gitignored. The file uses
|
||||
per-run section headers so it doubles as run history:
|
||||
|
||||
```markdown
|
||||
# clean-house run history
|
||||
|
||||
## 2026-05-06T13:42Z
|
||||
fired: RULE-ANCHOR-DRIFT(1) RULE-SUNSET-WITHOUT-TICKET(1)
|
||||
applied: RULE-ANCHOR-DRIFT(1) RULE-SUNSET-WITHOUT-TICKET(1)
|
||||
skipped:
|
||||
- RULE-ANCHOR-DRIFT D-8:#q-1-markdown-mirror-for-tickets
|
||||
|
||||
## 2026-05-04T11:10Z
|
||||
fired: ...
|
||||
```
|
||||
|
||||
Each section starts with a UTC ISO-8601 timestamp. `fired`
|
||||
counts findings detected; `applied` counts findings the user
|
||||
acted on; `skipped` lists `<rule-id> <finding-id>` pairs.
|
||||
|
||||
**Promotion-candidate computation.** Scan the most recent N=7
|
||||
sections (or all sections if fewer). A rule is a promotion
|
||||
candidate if it `fired` in ≥ 5 of those sections. Surface the
|
||||
candidates in the summary; do not auto-promote.
|
||||
|
||||
10. **Emit the summary** (see Output below).
|
||||
|
||||
## Question phrasing
|
||||
|
||||
Always include the rule ID in the prompt so the user learns what's
|
||||
being checked.
|
||||
|
||||
**Mechanical (batched):**
|
||||
> RULE-ANCHOR-DRIFT: 12 cross-reference anchors point to headings that
|
||||
> no longer exist (renamed or deleted). Apply all 12 fixes? Options:
|
||||
> apply all / show diff first / skip this rule / stop asking, summarize
|
||||
> the rest.
|
||||
|
||||
**Judgment (individual):**
|
||||
> RULE-SUNSET-WITHOUT-TICKET: D-59 mentions "must track dugite-native
|
||||
> releases for security updates" but has no linked T. Options: file T
|
||||
> now / draft a T description first / mark as already covered (add note
|
||||
> to D) / skip / stop asking.
|
||||
|
||||
Phrasing rules:
|
||||
|
||||
- **Always include "stop asking, summarize the rest."** Sessions
|
||||
without bandwidth for full review need an escape. Forcing answers
|
||||
to every question kills the tool.
|
||||
- **Always include "skip."** Skipped findings go to the ledger; three
|
||||
consecutive skips of the same finding promote the next prompt.
|
||||
- **Mechanical batches; judgment doesn't.** "Apply all 12 fixes?" is
|
||||
one decision against a clear delta. Twelve individual prompts defeat
|
||||
the point. Conversely, judgment findings are each separate work —
|
||||
presenting them as a batch hides cost.
|
||||
|
||||
## What this skill does NOT do
|
||||
|
||||
- Does not file tickets without asking.
|
||||
- Does not rewrite D-record body prose, only metadata fields and
|
||||
cross-reference links.
|
||||
- Does not touch records under `legacy/`.
|
||||
- Does not run `pql decisions validate` *as a fix* — only as a
|
||||
precondition gate.
|
||||
- Does not promote rules to `pql decisions validate` automatically.
|
||||
Promotion is a human decision; this skill only flags candidates
|
||||
("RULE-X has fired in 5 of the last 7 runs — consider promoting").
|
||||
|
||||
## Non-interactive context
|
||||
|
||||
If invoked without an interactive `AskUserQuestion` surface (a CI run,
|
||||
a batch script), refuse to apply judgment fixes. Apply mechanical
|
||||
fixes only if the user explicitly says so via the trigger phrase
|
||||
("auto-apply mechanical, skip judgment"); otherwise emit the summary
|
||||
and exit without mutating files.
|
||||
|
||||
## Output
|
||||
|
||||
At the end, emit a summary block:
|
||||
|
||||
```
|
||||
clean-house — sweep complete
|
||||
────────────────────────────
|
||||
Files scanned: 11
|
||||
Records parsed: 62 D, 23 Q, 11 R
|
||||
Conventions: heading_level=3 backlink="Resolved → "
|
||||
Mechanical fixes: 14 applied, 0 deferred
|
||||
Judgment findings: 3 acted on, 2 deferred (in state file)
|
||||
Promotion candidates: RULE-ANCHOR-DRIFT (5/7 runs)
|
||||
Touched files: decisions/architecture.md, decisions/questions.md
|
||||
```
|
||||
|
||||
If this is the first run (no prior history) or there are fewer
|
||||
than 5 prior runs, `Promotion candidates:` reports
|
||||
`(none — N/7 runs of history)` instead of decorative empty content.
|
||||
|
||||
The summary names the touched files so the next commit message can be
|
||||
honest about what changed. The skill does not commit on its own.
|
||||
|
||||
## Versioning
|
||||
|
||||
clean-house ships embedded in the pql binary; its version is the
|
||||
pql version (`pql --version`). To see what changed and when, read
|
||||
`CHANGELOG.md` or `git log internal/skill/clean-house/` in the pql
|
||||
repo. Keep this file and `references/rules.md` consistent with
|
||||
each other when iterating — they're the contract.
|
||||
@@ -0,0 +1,386 @@
|
||||
# clean-house rule catalog
|
||||
|
||||
The rules clean-house runs and the reasoning behind each. New rules
|
||||
land here as their own entry. Catalog churn — additions, refinements,
|
||||
retirements — is tracked in pql's `CHANGELOG.md` and `git log
|
||||
internal/skill/clean-house/`. When a rule is retired, leave a
|
||||
**Retired** stub (ID + one-line reason + retirement commit) so the
|
||||
trail of "we used to check X" is recoverable from this file alone.
|
||||
|
||||
## Reading a rule
|
||||
|
||||
Each entry below has:
|
||||
|
||||
- **ID** — Stable identifier (e.g. `RULE-ANCHOR-DRIFT`). Used in
|
||||
AskUserQuestion prompts and skip-ledger entries so the user learns
|
||||
what's being checked.
|
||||
- **Category** — `mechanical` or `judgment`. Drives whether findings
|
||||
batch into one prompt or each get their own.
|
||||
- **Finding ID** — Format string for the per-finding stable
|
||||
identifier. Skip-ledger entries are written as `<rule-id>
|
||||
<finding-id> <ISO-date>`; subsequent runs use the same format to
|
||||
recognize "same finding skipped 3 runs in a row" and escalate.
|
||||
Use only stable inputs (record IDs, file paths, slug strings, body
|
||||
hashes) — never line numbers or timestamps.
|
||||
- **Detection** — Concrete steps to find violations. Names the
|
||||
pql command or file primitive used.
|
||||
- **Fix** — Mechanical: deterministic action. Judgment: the prompt
|
||||
option list and what each option does.
|
||||
- **Why** — One paragraph on the failure mode this rule guards
|
||||
against. Future-you reading the rule benefits from the reasoning,
|
||||
not just the check.
|
||||
|
||||
---
|
||||
|
||||
## RULE-ANCHOR-DRIFT
|
||||
|
||||
**Category:** mechanical
|
||||
|
||||
**Finding ID:** `<source-record>:<link-target>` (e.g.
|
||||
`D-8:#q-1-markdown-mirror-for-tickets` or
|
||||
`D-8:questions.md#q-1`).
|
||||
|
||||
**Detection:**
|
||||
|
||||
Anchor-only markdown links (`[text](#slug)`) resolve against the
|
||||
**source file's full heading set**, not the body of a single record
|
||||
— record-level headings (`### D-N: …`) live as siblings in the
|
||||
file and are valid link targets. The previous detection (which used
|
||||
`pql decisions read`'s body-only `headings` array) missed these
|
||||
and produced false positives.
|
||||
|
||||
Per source body:
|
||||
|
||||
1. Open the source body's file (`<vault>/<file_path>` from
|
||||
`pql decisions list`) and extract every ATX heading. Build a
|
||||
slug index for the file using the GFM convention (lowercase,
|
||||
hyphenate spaces, drop punctuation, disambiguate duplicates with
|
||||
`-1`/`-2`). Cache per file — every record in that file uses the
|
||||
same index.
|
||||
2. For each `[text](target)` link in the body:
|
||||
- Anchor-only (`#slug`): check `slug` against the **source file's**
|
||||
index. Missing → flag.
|
||||
- Cross-file (`path.md#slug`): resolve `path.md` relative to the
|
||||
source file's directory; check `slug` against that file's
|
||||
index. Missing file or missing slug → flag.
|
||||
|
||||
**Fix:**
|
||||
|
||||
If the slug exists in a sibling file's index (same directory) and
|
||||
the link is anchor-only, rewrite to `path.md#slug` (the canonical
|
||||
cross-file form). If the slug exists in the source file's index
|
||||
with edit-distance ≤ 2 from the link target, rewrite to the
|
||||
matched slug. Otherwise downgrade to judgment ("no auto-fix; the
|
||||
heading was removed, not renamed") with options: drop the link /
|
||||
point elsewhere / mark as intentionally dangling.
|
||||
|
||||
**Fix:**
|
||||
|
||||
If exactly one heading with a closely-matching slug exists (slug
|
||||
edit-distance ≤ 2 or substring match), rewrite the link to point at
|
||||
that heading. If no plausible match exists, downgrade the finding to
|
||||
judgment ("no auto-fix; the heading was removed, not renamed") and
|
||||
prompt the user with options: drop the link / point elsewhere / mark
|
||||
as intentionally dangling.
|
||||
|
||||
**Why:**
|
||||
|
||||
When a heading is renamed for clarity, every cross-reference pointing
|
||||
at the old slug silently breaks. The rendered docs still look fine —
|
||||
the link just goes nowhere. Without periodic sweeps these decay
|
||||
indefinitely; the cost of the sweep is small compared to the cost of
|
||||
a reader following a dead link and losing trust in the index.
|
||||
|
||||
---
|
||||
|
||||
## RULE-MISSING-Q-BACKLINK
|
||||
|
||||
**Category:** mechanical
|
||||
|
||||
**Finding ID:** `<d-id>:<q-id>` (e.g. `D-7:Q-2`). Order is always
|
||||
D-first regardless of which side is missing the backlink.
|
||||
|
||||
**Detection:**
|
||||
|
||||
For each D record body, find lines of the shape `Resolves: Q-N`
|
||||
(plain text, typically in the metadata block at the top of the
|
||||
record). For each Q-N referenced, fetch that Q record's body via
|
||||
`pql decisions read Q-N` and check whether it contains a line of the
|
||||
shape `Resolved → D-N` (or equivalent backlink phrasing — match the
|
||||
project's own convention; default pattern is `Resolved → D-N`).
|
||||
|
||||
Also check the reverse direction: any Q record claiming
|
||||
`Resolved → D-N` whose D-N body lacks `Resolves: Q-N`.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Insert the missing backlink in-place using `Edit`:
|
||||
|
||||
- Missing on the D side: add `Resolves: Q-N` to the D's metadata
|
||||
block (typically right after `Domain:` / `Status:` lines).
|
||||
- Missing on the Q side: append `Resolved → D-N` to the Q's status
|
||||
line (or in the conventional position for that project).
|
||||
|
||||
**Why:**
|
||||
|
||||
Bidirectional Q↔D links are the navigation backbone of the DQR
|
||||
system. When one side drifts, search-by-decision finds nothing for
|
||||
that question and search-by-question doesn't surface its resolution.
|
||||
The asymmetric state usually arises from a hand-edit on one record
|
||||
that forgot to update the other; the fix is purely mechanical because
|
||||
the correct content is already determined by the existing pointer
|
||||
in the other direction.
|
||||
|
||||
---
|
||||
|
||||
## RULE-RECORD-SORT
|
||||
|
||||
**Category:** mechanical
|
||||
|
||||
**Finding ID:** `<file-path>` (e.g. `decisions/architecture.md`).
|
||||
The whole file is one finding — sort applies to the file as a
|
||||
unit.
|
||||
|
||||
**Detection:**
|
||||
|
||||
For each `decisions/*.md` file, read it directly (no pql) and find
|
||||
all top-level `## D-N` / `## Q-N` / `## R-N` headings in order. Strip
|
||||
the prefix, parse the numeric suffix, and check whether the sequence
|
||||
is monotonically ascending within each ID family (D, Q, R kept
|
||||
separate — files commonly mix families).
|
||||
|
||||
Only flag a file if it is **otherwise tidy** — currently no out-of-
|
||||
order amendments interleaved with new records. The heuristic: if the
|
||||
sort would touch fewer than three records out of position, apply it;
|
||||
if more, downgrade to judgment ("this file looks deliberately
|
||||
arranged — confirm before reordering"). Three is a soft threshold; if
|
||||
the file's last commit message contains `WIP` or `do-not-sort`, skip.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Reorder the records within the file so each ID family is ascending.
|
||||
Preserve everything else (headings between record blocks, any prose
|
||||
prelude/postlude). Use `Edit` with full block replacement, not in-
|
||||
place line shuffling — easier to verify the diff.
|
||||
|
||||
**Why:**
|
||||
|
||||
Records added at the bottom of a file are easy to write but hard to
|
||||
find. Sorted IDs let a reader find D-37 by jumping to the
|
||||
two-thirds mark of the file rather than scanning. The cost of the
|
||||
sort is one reordering pass; the benefit is paid back on every
|
||||
subsequent read.
|
||||
|
||||
---
|
||||
|
||||
## RULE-EOF-NORMALIZATION
|
||||
|
||||
**Category:** mechanical
|
||||
|
||||
**Finding ID:** `<file-path>` (one finding per file).
|
||||
|
||||
**Detection:**
|
||||
|
||||
For each `decisions/*.md` file (and any other markdown the skill
|
||||
touched during this run), read directly. Flag if:
|
||||
|
||||
- File does not end with exactly one `\n`.
|
||||
- Any line contains trailing whitespace before its `\n`.
|
||||
|
||||
**Fix:**
|
||||
|
||||
Trim trailing whitespace from each line. Ensure exactly one trailing
|
||||
newline at end of file. Use `Edit` only if a violation was found —
|
||||
this rule must not produce a no-op diff.
|
||||
|
||||
**Why:**
|
||||
|
||||
Editor and git config drift causes whitespace creep that's invisible
|
||||
in rendering but pollutes diffs (every record edit ends up touching
|
||||
unrelated lines). A periodic normalization keeps future diffs clean
|
||||
without forcing per-editor enforcement on every contributor.
|
||||
|
||||
---
|
||||
|
||||
## RULE-SUNSET-WITHOUT-TICKET
|
||||
|
||||
**Category:** judgment
|
||||
|
||||
**Finding ID:** `<record-id>:<phrase-hash-8>` where phrase-hash-8 is
|
||||
the first 8 hex chars of `sha256(<matched-phrase>)`. Lets the
|
||||
ledger distinguish two sunset phrases in the same record.
|
||||
|
||||
**Detection:**
|
||||
|
||||
For each D record body (via `pql decisions read <id>`), grep for
|
||||
sunset-shaped phrases. Default regex set:
|
||||
|
||||
```
|
||||
(?i)\b(delete|remove|sunset|kill[ -]?switch|tear[ -]?down) when\b
|
||||
(?i)\bmust (track|monitor|watch|follow)\b
|
||||
(?i)\brevisit (when|after|once)\b
|
||||
(?i)\b(deprecate|retire) (when|after|once)\b
|
||||
```
|
||||
|
||||
For each match, run `pql ticket list --decision <id>`. If no tickets
|
||||
exist, the D has work-shaped intent without a tracked T — flag.
|
||||
|
||||
**Fix (prompt options):**
|
||||
|
||||
- **File T now** — Run `pql ticket new task "<phrase>" --decision <id>`,
|
||||
then prompt the user for a description (or call `pql ticket refine
|
||||
write <T> --description ...` after creation).
|
||||
- **Draft a T description first** — Open an `AskUserQuestion` for the
|
||||
description, then file as above.
|
||||
- **Mark as already covered** — Add a note to the D body
|
||||
("Tracked under T-N") via `Edit`. Skill does not assert which T;
|
||||
user provides the ID.
|
||||
- **Skip** — Adds to the ledger.
|
||||
- **Stop asking** — Skip remaining sunset findings, summarize.
|
||||
|
||||
**Why:**
|
||||
|
||||
Sunset-shaped intent ("we'll handle X when Y happens") is the most
|
||||
common source of accumulated debt in a DQR system: the trigger
|
||||
condition arrives, nobody remembers the D, the work doesn't happen.
|
||||
Linking each sunset to a T is the simplest defense — a T is a
|
||||
backlog item that surfaces in `plan whatsnext` / `plan board`. The
|
||||
fix is judgment because the right action depends on whether the D's
|
||||
condition is still relevant, whether it's already covered, and what
|
||||
the right scope is for the resulting T.
|
||||
|
||||
---
|
||||
|
||||
## RULE-FILE-OVER-THRESHOLD
|
||||
|
||||
**Category:** judgment
|
||||
|
||||
**Finding ID:** `<file-path>` (one finding per file; threshold
|
||||
choice is judgment, not a per-line problem).
|
||||
|
||||
**Detection:**
|
||||
|
||||
For each `decisions/*.md` file, count lines (via `wc -l` or
|
||||
equivalent). Default threshold: **350 lines**. Configurable per
|
||||
project — read `decisions/.clean-house.yaml` if present, key
|
||||
`file_threshold`. Fall back to 350.
|
||||
|
||||
**Fix (prompt options):**
|
||||
|
||||
- **Split now (which axis?)** — Prompt for the split axis: by ID
|
||||
family (D/Q/R), by domain, by date range, custom. Then perform the
|
||||
split: create new file(s), move records, update any anchor links
|
||||
pointing into the moved records (run RULE-ANCHOR-DRIFT in apply
|
||||
mode against the affected files after the split).
|
||||
- **Accept and raise the threshold** — Update
|
||||
`decisions/.clean-house.yaml`'s `file_threshold` to the next
|
||||
reasonable round number above the current line count.
|
||||
- **Defer** — Skip until next run.
|
||||
|
||||
**Why:**
|
||||
|
||||
A single decisions file growing past ~350 lines is the point at which
|
||||
linear scanning starts to lose to grep, and where the cost of
|
||||
splitting (renaming anchors) is still small. Beyond ~600 lines the
|
||||
split cost compounds. The threshold is judgment because some projects
|
||||
deliberately keep one file per domain and accept the size; others
|
||||
split aggressively. The skill surfaces the question; it doesn't
|
||||
decide.
|
||||
|
||||
---
|
||||
|
||||
## RULE-DEAD-FILE-REFERENCE
|
||||
|
||||
**Category:** judgment
|
||||
|
||||
**Finding ID:** `<record-id>:<path-token>` (path token as written
|
||||
in the body, not resolved).
|
||||
|
||||
**Detection:**
|
||||
|
||||
For each D record body (via `pql decisions read <id>`), grep for
|
||||
path-shaped tokens. Default regex:
|
||||
|
||||
```
|
||||
\b([\w./-]+\.(md|go|py|sql|yaml|yml|toml))\b
|
||||
```
|
||||
|
||||
The first detection pass produced 6/6 false positives in
|
||||
real-world use; the regex is necessary but not sufficient. For
|
||||
each match, apply the filters below in order — if any matches,
|
||||
**skip without flagging**:
|
||||
|
||||
1. **Placeholder filter.** Token contains `T-NNN`, `D-NNN`,
|
||||
`Q-NNN`, `R-NNN`, `...`, `<`, `>`, or `*` — it's a pattern,
|
||||
not a real path.
|
||||
2. **Source-relative resolution.** Resolve the token against the
|
||||
source file's directory (`<vault>/<file_path>`'s dir). If
|
||||
`[ -e <resolved> ]`, the reference is live.
|
||||
3. **Repo-relative resolution.** If `[ -e <token> ]` from the
|
||||
repo root, the reference is live.
|
||||
4. **Basename-fallback.** Run `find <repo-root> -name <basename>`
|
||||
(where basename is the last path component). If exactly one
|
||||
match exists, treat the reference as live and emit a low-
|
||||
priority "consider rewriting to the absolute path" note (not a
|
||||
judgment finding). If multiple matches exist, do not flag —
|
||||
the token is too ambiguous.
|
||||
|
||||
Only after all four filters miss does the reference qualify as
|
||||
truly dead and warrant a judgment prompt.
|
||||
|
||||
**Fix (prompt options):**
|
||||
|
||||
- **Update reference** — Prompt for the new path; rewrite the
|
||||
reference via `Edit`. If the user types a path, validate it exists
|
||||
before applying.
|
||||
- **Mark superseded** — Add a note to the D ("This decision
|
||||
references files no longer present; the underlying constraint
|
||||
was retired by D-N") and prompt for the superseding D ID.
|
||||
- **Defer** — Skip until next run.
|
||||
|
||||
**Why:**
|
||||
|
||||
Decisions reference code paths to ground their reasoning in the
|
||||
codebase that motivated them. When the code moves or gets deleted,
|
||||
the D's reasoning becomes harder to verify. Dead refs are a signal
|
||||
that either the decision should be updated or the decision itself is
|
||||
no longer load-bearing — both are judgment calls the user needs to
|
||||
make.
|
||||
|
||||
---
|
||||
|
||||
## RULE-STALE-OPEN-Q
|
||||
|
||||
**Category:** judgment
|
||||
|
||||
**Finding ID:** `<q-id>` (one finding per Q-record; staleness
|
||||
threshold is global per run, so a Q is either stale or not).
|
||||
|
||||
**Detection:**
|
||||
|
||||
Run `pql decisions list --type question --status open`. For each
|
||||
returned record, check the `date` field. Default staleness threshold:
|
||||
**60 days** (configurable via `decisions/.clean-house.yaml` key
|
||||
`stale_open_q_days`). Flag any Q with `date` older than the
|
||||
threshold.
|
||||
|
||||
**Fix (prompt options):**
|
||||
|
||||
- **Still relevant** — Touch the Q's `date` field to today, optionally
|
||||
add a one-line "still open as of YYYY-MM-DD: <reason>" to the body.
|
||||
- **Nudge to load-bearing index** — If the project keeps a
|
||||
load-bearing Qs list (a curated index of unresolved questions
|
||||
affecting current work), prompt for inclusion and add the Q there.
|
||||
- **Mark withdrawn** — Change the Q's status to `withdrawn` (the DQR
|
||||
system's terminal state for "no longer worth answering"); the
|
||||
user supplies a one-line reason.
|
||||
- **Defer** — Skip until next run.
|
||||
|
||||
**Why:**
|
||||
|
||||
Open questions are useful when they're current; stale ones become
|
||||
ambient noise that drowns the signal of new questions. Most
|
||||
projects don't enforce explicit Q-lifecycle, so without a periodic
|
||||
prompt the open list grows forever. The right action depends on
|
||||
whether the question is still open in fact — not just in metadata —
|
||||
which only the user can confirm.
|
||||
@@ -0,0 +1,64 @@
|
||||
---
|
||||
name: clide
|
||||
description: >
|
||||
Use when you are running inside the clide IDE and want to observe or drive
|
||||
its live UI — panes, editor, files, git, readers, toasts, layout — through
|
||||
the `clide` CLI, or to find out what commands clide exposes. clide is the
|
||||
IDE hosting this session; it puts `clide` on your PATH and a per-workspace
|
||||
socket in `CLIDE_SOCK`. Start with `clide capabilities` to enumerate the
|
||||
live tool surface. Triggers: "what can clide do", "drive the clide UI",
|
||||
"open this in clide", "show the user", "toast", or invoking /clide.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash
|
||||
---
|
||||
|
||||
# Driving clide from the CLI
|
||||
|
||||
You are (often) running **inside clide** — a Flutter IDE that hosts this
|
||||
Claude session. It exposes its whole UI surface as a `clide <subsystem> <verb>`
|
||||
CLI on your PATH, talking to the running app over a per-workspace socket
|
||||
(`CLIDE_SOCK`). Every UI action the user can take has a CLI verb, and every
|
||||
verb's effect is observable — that is the parity contract (D-6). So you can
|
||||
*see what the user sees* and *show the user what you mean*.
|
||||
|
||||
## Discover the surface first — don't hard-code it
|
||||
|
||||
The authoritative, always-current list of commands is the app itself:
|
||||
|
||||
```
|
||||
clide capabilities
|
||||
```
|
||||
|
||||
It returns JSON: every registered command, split into `subsystem` + `verb`,
|
||||
with its argument schema (`positional` order + per-arg `type`/`required`/
|
||||
constraints) where one is declared. **Sourced from the live dispatcher
|
||||
registry, so it never drifts.** New panels/verbs appear here the moment they
|
||||
register — re-run it instead of trusting a remembered list (including this
|
||||
one). `clide <subsystem>` with no verb, or an unknown command, prints usage.
|
||||
|
||||
## The two halves of parity
|
||||
|
||||
- **Observe** — read the live UI state:
|
||||
- `clide status` — one-shot orientation: workspace, git, active editor
|
||||
buffer + selection, open reader docs, the panes the user sees, layout.
|
||||
- `clide pane list`, `clide editor active` — narrower snapshots.
|
||||
- **Drive** — make the UI do something:
|
||||
- `clide ui open <reader> <ref>` — open a doc in a GUI reader
|
||||
(`tickets`/`decisions` by id, `markdown`/`diff` by path): "look at this
|
||||
with me." `diff <path>` reveals the diff tab and scrolls to that file.
|
||||
- `clide ui toast "message" [--severity success|warning|error|info]` — raise
|
||||
a toast on the user's screen: "tests green", "push failed".
|
||||
- pane/editor/files/git verbs — see `clide capabilities` for the current set
|
||||
and their args.
|
||||
|
||||
## Conventions
|
||||
|
||||
- **Slots:** the layout has three content slots — `sidebar` (left), `workspace`
|
||||
(center, where Claude lives), `context` (right) — plus the bottom `statusbar`.
|
||||
Many verbs take a slot.
|
||||
- **Honest failures:** a drive verb with no live GUI returns a `toolError`
|
||||
("no live UI to drive"), not a hang. JSON on stdout; exit code conveys
|
||||
ok/usage/tool error.
|
||||
- **You only see what flows through clide.** Your own non-`clide` shell work
|
||||
(plain file reads, `make test`, `git`) is outside clide's view by design
|
||||
(D-83) — run it *through* `clide …` if you want clide to observe it.
|
||||
@@ -0,0 +1,53 @@
|
||||
---
|
||||
name: commit
|
||||
description: Create a well-formatted git commit with staged changes
|
||||
---
|
||||
|
||||
# Git Commit
|
||||
|
||||
Create a well-formatted commit with staged changes following best practices.
|
||||
|
||||
## Steps
|
||||
|
||||
1. Run `git status --porcelain` to check for changes
|
||||
2. If no staged changes, show unstaged files and ask what to stage
|
||||
3. Run `git diff --cached` to review staged changes
|
||||
4. Generate a commit message following Conventional Commits format:
|
||||
- `feat:` new feature
|
||||
- `fix:` bug fix
|
||||
- `docs:` documentation
|
||||
- `refactor:` code restructuring
|
||||
- `test:` adding tests
|
||||
- `chore:` maintenance
|
||||
5. Create commit with the message, adding Co-Authored-By trailer
|
||||
|
||||
## Commit Message Format
|
||||
|
||||
```
|
||||
<type>(<scope>): <short description>
|
||||
|
||||
<body - what and why, not how>
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
## Best Practices
|
||||
|
||||
- Warn about large commits (>500 lines changed)
|
||||
- Suggest splitting large changes into smaller commits
|
||||
- Never skip pre-commit hooks unless explicitly requested
|
||||
|
||||
## Changelog
|
||||
|
||||
After committing, update `CHANGELOG.md` following [Keep a Changelog](https://keepachangelog.com/en/1.1.0/) format:
|
||||
|
||||
1. Add entry under `[Unreleased]` section (create if not exists)
|
||||
2. Categorize changes:
|
||||
- `Added` - new features
|
||||
- `Changed` - changes to existing functionality
|
||||
- `Deprecated` - features marked for removal
|
||||
- `Removed` - removed features
|
||||
- `Fixed` - bug fixes
|
||||
- `Security` - security-related changes
|
||||
3. Write entries in imperative mood: "Add feature" not "Added feature"
|
||||
4. Reference issue numbers where applicable
|
||||
@@ -0,0 +1,151 @@
|
||||
---
|
||||
name: d2-diagram
|
||||
description: >
|
||||
Generate technical diagrams using d2 (text-to-diagram CLI). Use when the
|
||||
user says "create a diagram", "draw architecture", "make a flowchart",
|
||||
"diagram this", "render d2", "d2", "data flow diagram", "entity relationship",
|
||||
"state machine", "sequence diagram", "UI flow", or invokes /d2-diagram.
|
||||
Produces .d2 source files and renders them to PNG. Also use when asked
|
||||
to update, re-render, or batch render existing diagrams.
|
||||
---
|
||||
|
||||
# d2 Diagram Generation
|
||||
|
||||
Generate technical diagrams from text using d2 (v0.7.1). Pure CLI, no
|
||||
external dependencies beyond the d2 binary.
|
||||
|
||||
**Binary:** `/home/linuxbrew/.linuxbrew/bin/d2`
|
||||
|
||||
## Project Defaults
|
||||
|
||||
| Setting | Value | Override |
|
||||
|---------|-------|----------|
|
||||
| Theme | 200 (Dark Mauve) | `--theme N` |
|
||||
| Layout | dagre | `--layout elk` |
|
||||
| Padding | 100px | — |
|
||||
| Format | PNG | `--svg` |
|
||||
|
||||
## Output Convention
|
||||
|
||||
```
|
||||
docs/diagrams/
|
||||
architecture/ # System architecture, IPC, component layout
|
||||
data-flow/ # Sequence diagrams, data pipelines
|
||||
entity/ # ER diagrams, ECS component schemas
|
||||
state/ # State machines, behavior trees
|
||||
ui/ # UI navigation flow, screen transitions
|
||||
```
|
||||
|
||||
Both `.d2` source and `.png` output are tracked in git.
|
||||
|
||||
## Single Diagram Workflow
|
||||
|
||||
1. **Determine category** — architecture, data-flow, entity, state, or ui
|
||||
2. **Read template** — `references/diagram-templates.md` for the matching category
|
||||
3. **Read syntax** — `references/d2-syntax-guide.md` if unfamiliar with d2 syntax
|
||||
4. **Write .d2 source** — to `docs/diagrams/{category}/{name}.d2`
|
||||
5. **Validate** — `.claude/skills/d2-diagram/scripts/d2-render.sh validate {file}`
|
||||
6. **Render** — `.claude/skills/d2-diagram/scripts/d2-render.sh {file}`
|
||||
7. **Read SVG** — verify the output, present to user
|
||||
|
||||
### Script Usage
|
||||
|
||||
```bash
|
||||
# Render with project defaults
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/ipc-bridge.d2
|
||||
|
||||
# Validate syntax only
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh validate docs/diagrams/architecture/ipc-bridge.d2
|
||||
|
||||
# Auto-format source
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh fmt docs/diagrams/architecture/ipc-bridge.d2
|
||||
|
||||
# Sketch mode (hand-drawn look for drafts)
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/ui/flow.d2 --sketch
|
||||
|
||||
# Light theme (for printable docs)
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/entity/schema.d2 --theme 0
|
||||
|
||||
# SVG output (if specifically needed)
|
||||
.claude/skills/d2-diagram/scripts/d2-render.sh docs/diagrams/architecture/overview.d2 --svg
|
||||
```
|
||||
|
||||
## Batch Render
|
||||
|
||||
Re-render all diagrams after theme or style changes:
|
||||
|
||||
```bash
|
||||
# All diagrams
|
||||
.claude/skills/d2-diagram/scripts/d2-batch.sh
|
||||
|
||||
# One category
|
||||
.claude/skills/d2-diagram/scripts/d2-batch.sh docs/diagrams/architecture/
|
||||
|
||||
# Preview what would render
|
||||
.claude/skills/d2-diagram/scripts/d2-batch.sh --dry-run
|
||||
|
||||
# Force re-render everything
|
||||
.claude/skills/d2-diagram/scripts/d2-batch.sh --force
|
||||
```
|
||||
|
||||
Batch skips files whose PNG is newer than the `.d2` source unless `--force`.
|
||||
|
||||
## Advanced Patterns
|
||||
|
||||
### Variables for consistent styling
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-bg: "#2a3040"
|
||||
color-stroke: "#333340"
|
||||
color-text: "#c8d0e0"
|
||||
color-accent: "#c8d8f0"
|
||||
}
|
||||
```
|
||||
|
||||
### Multi-board (layers)
|
||||
|
||||
```d2
|
||||
# Base diagram here
|
||||
|
||||
layers: {
|
||||
detailed: {
|
||||
# More detailed view
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Sequence diagrams
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
client: Godot Client
|
||||
server: Rust Server
|
||||
|
||||
client -> server: TickRequest(delta)
|
||||
server -> client: WorldState(entities)
|
||||
```
|
||||
|
||||
### Imports
|
||||
|
||||
Split shared definitions into a separate file and import:
|
||||
|
||||
```d2
|
||||
...@shared-defs.d2
|
||||
```
|
||||
|
||||
## Agent Guidance
|
||||
|
||||
- **Qatux** — Architecture decision records, system overview diagrams, data
|
||||
schemas. Prefer architecture and entity templates.
|
||||
- **Tyre** — IPC bridge, ECS system flow, chunk loading pipeline, perception
|
||||
system data flow. Prefer architecture and data-flow templates.
|
||||
- **Araminta** — UI navigation flow, screen transitions, component hierarchy.
|
||||
Prefer UI flow template.
|
||||
|
||||
## References
|
||||
|
||||
- `references/d2-syntax-guide.md` — Language quick reference (shapes, edges,
|
||||
containers, styling, variables). Read when unfamiliar with d2 syntax.
|
||||
- `references/diagram-templates.md` — Five category templates with complete
|
||||
d2 source examples. Read when starting a new diagram.
|
||||
@@ -0,0 +1,212 @@
|
||||
# D2 Language Quick Reference
|
||||
|
||||
## Nodes
|
||||
|
||||
```d2
|
||||
server # Implicit label from key
|
||||
server: Simulation Server # Explicit label
|
||||
server: Simulation Server { # With properties
|
||||
shape: hexagon
|
||||
style.fill: "#2d3436"
|
||||
}
|
||||
```
|
||||
|
||||
## Edges
|
||||
|
||||
```d2
|
||||
a -> b # Directed
|
||||
a <- b # Reverse directed
|
||||
a <-> b # Bidirectional
|
||||
a -- b # Undirected
|
||||
a -> b: "label" # Labeled edge
|
||||
a -> b -> c # Chained
|
||||
```
|
||||
|
||||
## Containers (nesting)
|
||||
|
||||
```d2
|
||||
infrastructure: {
|
||||
server: Simulation Server
|
||||
database: State Store {
|
||||
shape: cylinder
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Shapes
|
||||
|
||||
| Shape | Use for |
|
||||
|-------|---------|
|
||||
| `rectangle` | Default. Components, modules, generic. |
|
||||
| `hexagon` | Systems, services, major components. |
|
||||
| `cylinder` | Databases, storage, persistent state. |
|
||||
| `diamond` | Decisions, conditions, branch points. |
|
||||
| `oval` / `circle` | Start/end states, events. |
|
||||
| `cloud` | External systems, networks. |
|
||||
| `person` | Actors, users, NPCs. |
|
||||
| `queue` | Message queues, buffers. |
|
||||
| `page` | Documents, files. |
|
||||
| `package` | Packages, modules, crates. |
|
||||
| `sql_table` | Database tables, ECS component schemas. |
|
||||
| `class` | Class diagrams, ECS system definitions. |
|
||||
| `code` | Code blocks (set `language` property). |
|
||||
| `markdown` | Rich text blocks. |
|
||||
|
||||
## SQL Tables
|
||||
|
||||
```d2
|
||||
entity: {
|
||||
shape: sql_table
|
||||
id: u64 {constraint: primary_key}
|
||||
name: String
|
||||
position: Vec2
|
||||
faction_id: u64 {constraint: foreign_key}
|
||||
}
|
||||
```
|
||||
|
||||
## Class Diagrams
|
||||
|
||||
```d2
|
||||
perception_system: {
|
||||
shape: class
|
||||
+run(world: &mut World)
|
||||
-calculate_los(entity: Entity): HashSet<Entity>
|
||||
#update_knowledge(entity: Entity, seen: HashSet<Entity>)
|
||||
}
|
||||
```
|
||||
|
||||
## Sequence Diagrams
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
client: Godot Client
|
||||
server: Rust Server
|
||||
|
||||
client -> server: TickRequest(delta)
|
||||
server -> server: run ECS systems
|
||||
server -> client: WorldState(entities)
|
||||
```
|
||||
|
||||
## Styling
|
||||
|
||||
```d2
|
||||
node: Label {
|
||||
style: {
|
||||
fill: "#2d3436"
|
||||
stroke: "#333340"
|
||||
stroke-width: 2
|
||||
stroke-dash: 5 # Dashed line
|
||||
opacity: 0.8
|
||||
font-size: 14
|
||||
font-color: "#c8d0e0"
|
||||
bold: true
|
||||
italic: false
|
||||
border-radius: 4
|
||||
shadow: true
|
||||
3d: true # Rectangles only
|
||||
multiple: true # Stacked appearance
|
||||
double-border: true # Rectangles/ovals only
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Edge styling
|
||||
|
||||
```d2
|
||||
a -> b: {
|
||||
style: {
|
||||
stroke: "#c8d8f0"
|
||||
stroke-width: 2
|
||||
stroke-dash: 5
|
||||
opacity: 0.8
|
||||
animated: true # Animated flow
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Variables
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-bg: "#1a1e24"
|
||||
color-stroke: "#333340"
|
||||
color-text: "#c8d0e0"
|
||||
color-accent: "#c8d8f0"
|
||||
}
|
||||
|
||||
node: {
|
||||
style.fill: ${color-bg}
|
||||
style.stroke: ${color-stroke}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
```
|
||||
|
||||
## Direction
|
||||
|
||||
```d2
|
||||
direction: right # left-to-right (default for dagre)
|
||||
direction: down # top-to-bottom
|
||||
direction: up
|
||||
direction: left
|
||||
```
|
||||
|
||||
## Imports
|
||||
|
||||
```d2
|
||||
...@shared-defs.d2 # Spread import (inline all definitions)
|
||||
```
|
||||
|
||||
## Icons
|
||||
|
||||
```d2
|
||||
node: Label {
|
||||
icon: https://icons.terrastruct.com/essentials/time.svg
|
||||
}
|
||||
```
|
||||
|
||||
## Layers (multi-board)
|
||||
|
||||
```d2
|
||||
# Base diagram content here
|
||||
|
||||
layers: {
|
||||
detailed: {
|
||||
# More detailed view
|
||||
}
|
||||
simplified: {
|
||||
# Simplified overview
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Scenarios (animated transitions)
|
||||
|
||||
```d2
|
||||
# Base state
|
||||
|
||||
scenarios: {
|
||||
alert: {
|
||||
# Changes from base for alert state
|
||||
}
|
||||
combat: {
|
||||
# Changes from base for combat state
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Comments
|
||||
|
||||
```d2
|
||||
# This is a comment
|
||||
node: Label # Inline comment
|
||||
```
|
||||
|
||||
## Project Colors (from visual-grammar-v01.md)
|
||||
|
||||
| Constant | Hex | Usage |
|
||||
|----------|-----|-------|
|
||||
| Zone 1 floor | `#1a1e24` | Dark backgrounds |
|
||||
| Zone 1 wall | `#2a3040` | Component fill |
|
||||
| Outline standard | `#333340` | Borders, strokes |
|
||||
| Insert chrome | `#c8d0e0` | Text, labels |
|
||||
| Zone 1 fixture | `#c8d8f0` | Accents, highlights |
|
||||
@@ -0,0 +1,251 @@
|
||||
# Diagram Templates
|
||||
|
||||
Copy, adapt, and render. Each template uses project colors from visual-grammar-v01.md.
|
||||
|
||||
---
|
||||
|
||||
## 1. Architecture Diagram
|
||||
|
||||
System components, relationships, communication channels.
|
||||
|
||||
**When to use:** IPC bridge, perception pipeline, chunk loading, ECS system layout, client-server architecture.
|
||||
|
||||
**Agents:** Tyre (system architecture), Qatux (architecture decision records).
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-bg: "#2a3040"
|
||||
color-stroke: "#333340"
|
||||
color-text: "#c8d0e0"
|
||||
color-accent: "#c8d8f0"
|
||||
}
|
||||
|
||||
direction: right
|
||||
|
||||
client: Godot Client {
|
||||
shape: hexagon
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
rendering: Rendering {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
ui: UI Layer {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
bridge: IPC Bridge {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
style.stroke: ${color-accent}
|
||||
}
|
||||
}
|
||||
|
||||
server: Rust Server {
|
||||
shape: hexagon
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
ecs: bevy_ecs {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
perception: Perception {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
bridge: IPC Bridge {
|
||||
style.fill: ${color-bg}
|
||||
style.font-color: ${color-text}
|
||||
style.stroke: ${color-accent}
|
||||
}
|
||||
}
|
||||
|
||||
client.bridge -> server.bridge: "stdin/stdout" {
|
||||
style.stroke: ${color-accent}
|
||||
style.stroke-dash: 5
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Entity Relationship
|
||||
|
||||
Data schemas, ECS components, knowledge graph structure.
|
||||
|
||||
**When to use:** Database tables, component definitions, entity relationships, knowledge store schema.
|
||||
|
||||
**Agents:** Tyre (ECS component design), Qatux (schema documentation).
|
||||
|
||||
```d2
|
||||
entity: Entity {
|
||||
shape: sql_table
|
||||
id: u64 {constraint: primary_key}
|
||||
name: String
|
||||
faction_id: u64 {constraint: foreign_key}
|
||||
}
|
||||
|
||||
position: Position {
|
||||
shape: sql_table
|
||||
entity_id: u64 {constraint: foreign_key}
|
||||
x: f32
|
||||
y: f32
|
||||
chunk_id: u32
|
||||
}
|
||||
|
||||
knowledge: KnowledgeEntry {
|
||||
shape: sql_table
|
||||
observer_id: u64 {constraint: foreign_key}
|
||||
subject_id: u64 {constraint: foreign_key}
|
||||
fact_type: FactType
|
||||
confidence: f32
|
||||
last_seen_tick: u64
|
||||
}
|
||||
|
||||
entity.id -> position.entity_id
|
||||
entity.id -> knowledge.observer_id
|
||||
entity.id -> knowledge.subject_id
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Sequence / Data Flow
|
||||
|
||||
Ordered interactions between systems over time.
|
||||
|
||||
**When to use:** IPC message flow, tick processing, perception update cycle, dialogue system exchanges.
|
||||
|
||||
**Agents:** Tyre (system interaction design), Qatux (protocol documentation).
|
||||
|
||||
```d2
|
||||
shape: sequence_diagram
|
||||
|
||||
client: Godot Client
|
||||
bridge: IPC Bridge
|
||||
server: Rust Server
|
||||
ecs: ECS Systems
|
||||
|
||||
client -> bridge: TickRequest(delta, input)
|
||||
bridge -> server: deserialize + dispatch
|
||||
server -> ecs: run_systems(delta)
|
||||
ecs -> ecs: perception, AI, physics
|
||||
ecs -> server: collect WorldState
|
||||
server -> bridge: serialize WorldState
|
||||
bridge -> client: WorldState(entities, events)
|
||||
client -> client: update rendering
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. State Machine
|
||||
|
||||
Entity states, transitions, conditions.
|
||||
|
||||
**When to use:** NPC behavior states, game mode transitions, dialogue state, investigation phases.
|
||||
|
||||
**Agents:** Tyre (behavior system design), Qatux (state documentation).
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-state: "#2a3040"
|
||||
color-text: "#c8d0e0"
|
||||
color-edge: "#c8d8f0"
|
||||
color-decision: "#333340"
|
||||
}
|
||||
|
||||
idle: Idle {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
|
||||
alert: Alert {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
|
||||
investigate: Investigate {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
|
||||
combat: Combat {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
style.stroke: "#f0b840"
|
||||
}
|
||||
|
||||
flee: Flee {
|
||||
style.fill: ${color-state}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
|
||||
idle -> alert: "stimulus detected" { style.stroke: ${color-edge} }
|
||||
alert -> investigate: "stimulus confirmed" { style.stroke: ${color-edge} }
|
||||
alert -> idle: "timeout / stimulus lost" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||
investigate -> combat: "threat confirmed" { style.stroke: "#f0b840" }
|
||||
investigate -> idle: "nothing found" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||
combat -> flee: "health < threshold" { style.stroke: "#f0b840" }
|
||||
combat -> idle: "threat eliminated" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||
flee -> idle: "safe distance reached" { style.stroke: ${color-edge}; style.stroke-dash: 5 }
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. UI Flow
|
||||
|
||||
Screen navigation, component hierarchy, interaction paths.
|
||||
|
||||
**When to use:** HUD layout relationships, menu navigation, dialogue flow, insert mode transitions.
|
||||
|
||||
**Agents:** Araminta (UI/visual design), Tyre (interface architecture), Qatux (UI documentation).
|
||||
|
||||
```d2
|
||||
vars: {
|
||||
color-screen: "#1a1e24"
|
||||
color-panel: "#2a3040"
|
||||
color-text: "#c8d0e0"
|
||||
color-nav: "#c8d8f0"
|
||||
}
|
||||
|
||||
gameplay: Gameplay {
|
||||
style.fill: ${color-screen}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
hud: HUD {
|
||||
style.fill: ${color-panel}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
minimap: Minimap
|
||||
monologue: Monologue Panel
|
||||
insert_display: Insert Display
|
||||
}
|
||||
|
||||
world: World View {
|
||||
style.fill: ${color-panel}
|
||||
style.font-color: ${color-text}
|
||||
}
|
||||
}
|
||||
|
||||
pause: Pause Menu {
|
||||
style.fill: ${color-screen}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
inventory: Inventory
|
||||
journal: Journal
|
||||
settings: Settings
|
||||
}
|
||||
|
||||
dialogue: Dialogue Mode {
|
||||
style.fill: ${color-screen}
|
||||
style.font-color: ${color-text}
|
||||
|
||||
speaker: Speaker Panel
|
||||
responses: Response List
|
||||
}
|
||||
|
||||
gameplay -> pause: "ESC" { style.stroke: ${color-nav} }
|
||||
pause -> gameplay: "ESC / Resume" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
||||
gameplay -> dialogue: "interact with NPC" { style.stroke: ${color-nav} }
|
||||
dialogue -> gameplay: "end conversation" { style.stroke: ${color-nav}; style.stroke-dash: 5 }
|
||||
```
|
||||
+84
@@ -0,0 +1,84 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
||||
RENDER="$SCRIPT_DIR/d2-render.sh"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [directory] [options]
|
||||
|
||||
Batch render all .d2 files in a directory. Skips files whose PNG is
|
||||
newer than the source unless --force is used.
|
||||
|
||||
Options:
|
||||
--dry-run List files that would be rendered
|
||||
--force Re-render even if SVG is up to date
|
||||
--theme N Override theme for all files
|
||||
|
||||
Examples:
|
||||
$(basename "$0") # All in docs/diagrams/
|
||||
$(basename "$0") docs/diagrams/architecture/ # One category
|
||||
$(basename "$0") --dry-run # Preview
|
||||
$(basename "$0") --force # Re-render everything
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
DIR="$REPO_ROOT/docs/diagrams"
|
||||
DRY_RUN=false
|
||||
FORCE=false
|
||||
EXTRA_ARGS=()
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--theme) EXTRA_ARGS+=(--theme "$2"); shift 2 ;;
|
||||
--help|-h) usage ;;
|
||||
*)
|
||||
if [[ -d "$1" ]] || [[ -d "$REPO_ROOT/$1" ]]; then
|
||||
DIR="$1"
|
||||
[[ "$DIR" != /* ]] && DIR="$REPO_ROOT/$DIR"
|
||||
else
|
||||
echo "Unknown option or directory: $1" >&2; exit 1
|
||||
fi
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
done
|
||||
|
||||
[[ ! -d "$DIR" ]] && { echo "ERROR: Directory not found: $DIR" >&2; exit 1; }
|
||||
|
||||
RENDERED=0
|
||||
SKIPPED=0
|
||||
FAILED=0
|
||||
|
||||
while IFS= read -r -d '' d2_file; do
|
||||
png_file="${d2_file%.d2}.png"
|
||||
|
||||
# Skip if PNG is newer than source (unless --force)
|
||||
if [[ "$FORCE" != true ]] && [[ -f "$png_file" ]] && [[ "$png_file" -nt "$d2_file" ]]; then
|
||||
SKIPPED=$((SKIPPED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
rel_path="${d2_file#"$REPO_ROOT/"}"
|
||||
|
||||
if [[ "$DRY_RUN" == true ]]; then
|
||||
echo "Would render: $rel_path"
|
||||
RENDERED=$((RENDERED + 1))
|
||||
continue
|
||||
fi
|
||||
|
||||
if "$RENDER" "$d2_file" "${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}"; then
|
||||
RENDERED=$((RENDERED + 1))
|
||||
else
|
||||
echo "FAILED: $rel_path" >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
fi
|
||||
done < <(find "$DIR" -name '*.d2' -print0 | sort -z)
|
||||
|
||||
echo ""
|
||||
echo "Batch complete: $RENDERED rendered, $SKIPPED skipped (up to date), $FAILED failed"
|
||||
+92
@@ -0,0 +1,92 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
D2="/home/linuxbrew/.linuxbrew/bin/d2"
|
||||
SCRIPT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR" && git rev-parse --show-toplevel)"
|
||||
|
||||
DEFAULT_THEME=200
|
||||
DEFAULT_LAYOUT="dagre"
|
||||
DEFAULT_PAD=100
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") [validate|fmt] <file.d2> [options]
|
||||
|
||||
Render a .d2 file to PNG with project defaults (theme $DEFAULT_THEME, $DEFAULT_LAYOUT layout).
|
||||
|
||||
Commands:
|
||||
validate <file> Check syntax without rendering
|
||||
fmt <file> Auto-format in place
|
||||
|
||||
Options:
|
||||
--theme N Override theme (default: $DEFAULT_THEME)
|
||||
--layout NAME Override layout engine (default: $DEFAULT_LAYOUT)
|
||||
--sketch Enable hand-drawn sketch mode
|
||||
--output PATH Override output path (default: input with .png extension)
|
||||
--svg Render to SVG instead of PNG
|
||||
|
||||
Examples:
|
||||
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2
|
||||
$(basename "$0") validate docs/diagrams/architecture/ipc-bridge.d2
|
||||
$(basename "$0") docs/diagrams/architecture/ipc-bridge.d2 --sketch --theme 0
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
[[ $# -lt 1 ]] && usage
|
||||
|
||||
# Parse subcommand
|
||||
SUBCMD=""
|
||||
case "$1" in
|
||||
validate|fmt)
|
||||
SUBCMD="$1"
|
||||
shift
|
||||
;;
|
||||
esac
|
||||
|
||||
[[ $# -lt 1 ]] && usage
|
||||
|
||||
INPUT="$1"
|
||||
shift
|
||||
|
||||
# Resolve to absolute path
|
||||
[[ "$INPUT" != /* ]] && INPUT="$REPO_ROOT/$INPUT"
|
||||
|
||||
[[ ! -f "$INPUT" ]] && { echo "ERROR: File not found: $INPUT" >&2; exit 1; }
|
||||
|
||||
# Handle subcommands
|
||||
if [[ -n "$SUBCMD" ]]; then
|
||||
"$D2" "$SUBCMD" "$INPUT"
|
||||
echo "OK: $SUBCMD $INPUT"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Parse render options
|
||||
THEME="$DEFAULT_THEME"
|
||||
LAYOUT="$DEFAULT_LAYOUT"
|
||||
SKETCH=""
|
||||
OUTPUT=""
|
||||
FORMAT="png"
|
||||
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--theme) THEME="$2"; shift 2 ;;
|
||||
--layout) LAYOUT="$2"; shift 2 ;;
|
||||
--sketch) SKETCH="-s"; shift ;;
|
||||
--output) OUTPUT="$2"; shift 2 ;;
|
||||
--svg) FORMAT="svg"; shift ;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
# Derive output path
|
||||
if [[ -z "$OUTPUT" ]]; then
|
||||
OUTPUT="${INPUT%.d2}.$FORMAT"
|
||||
fi
|
||||
|
||||
# Render
|
||||
"$D2" -t "$THEME" -l "$LAYOUT" --pad "$DEFAULT_PAD" $SKETCH "$INPUT" "$OUTPUT"
|
||||
|
||||
SIZE=$(stat --printf="%s" "$OUTPUT" 2>/dev/null || stat -f%z "$OUTPUT" 2>/dev/null)
|
||||
echo "Rendered: $OUTPUT ($(( SIZE / 1024 ))KB)"
|
||||
@@ -0,0 +1,222 @@
|
||||
---
|
||||
name: frame0-wireframe
|
||||
description: >
|
||||
Create and export UI wireframes using Frame0 (local desktop wireframing
|
||||
app with HTTP API). Use when the user says "create wireframe", "wireframe
|
||||
this", "mock up the UI", "draw a screen", "UI layout", "wireframe the HUD",
|
||||
"Frame0", "frame0", "export wireframe", or invokes /frame0-wireframe.
|
||||
Wireframes are authored as local JSON files (source of truth) and synced
|
||||
to Frame0 for rendering and export. Requires Frame0 to be running locally.
|
||||
---
|
||||
|
||||
# Frame0 Wireframe Generation
|
||||
|
||||
Create UI wireframes as JSON files, sync them to Frame0 for rendering, and
|
||||
export as PNG. Local JSON is the source of truth — Frame0 is the renderer.
|
||||
|
||||
**Frame0 is a renderer, not a workspace.** Treat it as disposable output.
|
||||
Push freely, delete test pages, keep it clean. Never pull from Frame0 unless
|
||||
the user explicitly says they have made edits in Frame0 and want to import
|
||||
them. The pull workflow exists for that case only — do not use it proactively.
|
||||
|
||||
**Prerequisite:** Frame0 desktop app must be running. If not available,
|
||||
stop and inform the user. Point to `references/setup-guide.md`.
|
||||
|
||||
## Health Check
|
||||
|
||||
Always check first:
|
||||
|
||||
```bash
|
||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
||||
```
|
||||
|
||||
## Core Workflow
|
||||
|
||||
Wireframing is a **design conversation, not a batch job.** Produce **one screen
|
||||
at a time**, show it, and **stop for the user's reaction** before authoring the
|
||||
next. Each shown screen is a checkpoint — never fan out a whole set of
|
||||
wireframes unprompted, even if the user described several screens up front.
|
||||
|
||||
1. **Health check** — verify Frame0 is running
|
||||
2. **Write wireframe JSON** — to `docs/design/wireframes/{category}/{name}.json`.
|
||||
Author **one** screen.
|
||||
3. **Push to Frame0** — `frame0-sync.py push <file.json>`
|
||||
4. **Export PNG** — `frame0-sync.py export <file.json> <output.png>`
|
||||
5. **Show it inline** — surface the exported PNG in the conversation so the
|
||||
user sees the result without opening Frame0. Always do this after an export,
|
||||
and again whenever you re-export or the user asks to see it.
|
||||
- **Inside clide** (the IDE hosting this session — `CLIDE_SOCK` is set):
|
||||
render it as a native image card in the live Claude pane with
|
||||
`clide image show <path> [--caption "…"]` (T-249). This is the *proper*
|
||||
way to put an image in the clide conversation — a bare `Read` only shows
|
||||
it in the transcript, not in the UI the user is looking at.
|
||||
- **Otherwise**: `Read` the PNG so it renders inline in the transcript.
|
||||
6. **STOP and wait for approval** — after showing a screen, pause and ask the
|
||||
user for feedback. Do **not** author, push, or export the next screen until
|
||||
they give an explicit go-ahead. Iterate on the current screen until they're
|
||||
happy, then proceed to the next one — looping back to step 2 for each.
|
||||
7. **Clean up** — delete test/scratch pages from Frame0 when done
|
||||
|
||||
> **Multi-screen requests are still one-at-a-time.** If the user asks for
|
||||
> several screens, treat it as a queue: build the first, show it, get approval,
|
||||
> then move on. The only exception is an explicit, unambiguous instruction to
|
||||
> generate a batch without stopping (and even then prefer the batch-export
|
||||
> dry-run + approval flow below).
|
||||
|
||||
### Scripts
|
||||
|
||||
| Script | Purpose |
|
||||
|--------|---------|
|
||||
| `scripts/frame0-sync.py` | **Primary.** Push/pull/export wireframes between JSON and Frame0 |
|
||||
| `scripts/frame0-cmd.sh` | Low-level API wrapper for ad-hoc commands |
|
||||
|
||||
## Wireframe JSON Format
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Dialogue Box",
|
||||
"shapes": {
|
||||
"panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 170, "top": 500, "width": 800, "height": 260,
|
||||
"fillColor": "#1a1e24",
|
||||
"strokeColor": "#333340",
|
||||
"corners": [8, 8, 8, 8]
|
||||
},
|
||||
"speaker": {
|
||||
"type": "Text",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 520,
|
||||
"text": "LERA KONSTANTIN",
|
||||
"fontColor": "#c8d0e0",
|
||||
"fontSize": 16
|
||||
},
|
||||
"btn-ask": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 670, "width": 370, "height": 30,
|
||||
"fillColor": "#2a3040",
|
||||
"strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
}
|
||||
},
|
||||
"connectors": {
|
||||
"flow-1": {
|
||||
"tailId": "panel",
|
||||
"headId": "btn-ask",
|
||||
"strokeColor": "#c8d8f0"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### Key rules
|
||||
|
||||
- **Shape IDs are stable local IDs** you control (e.g. `"panel"`, `"btn-ask"`)
|
||||
- **`parent`** references another local shape ID for nesting
|
||||
- **`type`** uses create-API names: `Rectangle`, `Ellipse`, `Text`, `Line`
|
||||
- **Colors** can be hex (`#2a3040`) or Frame0 theme tokens (`$slate6`)
|
||||
- After a pull, Frame0 returns its native vocabulary (`Box` for Rectangle,
|
||||
theme tokens for colors). The sync script handles the mapping transparently.
|
||||
- The `.idmap.json` mapping file (gitignored) tracks local ID ↔ Frame0 ID
|
||||
|
||||
### Sync commands
|
||||
|
||||
```bash
|
||||
SYNC=".claude/skills/frame0-wireframe/scripts/frame0-sync.py"
|
||||
|
||||
# Push local JSON to Frame0 (clears page, recreates all shapes)
|
||||
$SYNC push docs/design/wireframes/dialogue/dialogue-box.json
|
||||
|
||||
# Pull Frame0 page back to local JSON (preserves local IDs via mapping)
|
||||
$SYNC pull "Dialogue Box" docs/design/wireframes/dialogue/dialogue-box.json
|
||||
|
||||
# Push + export as PNG in one step
|
||||
$SYNC export docs/design/wireframes/dialogue/dialogue-box.json \
|
||||
docs/design/wireframes/dialogue/dialogue-box.png
|
||||
```
|
||||
|
||||
### Batch export
|
||||
|
||||
Use this when exporting multiple wireframes. It runs as a single Bash call,
|
||||
avoiding repeated permission prompts.
|
||||
|
||||
```bash
|
||||
BATCH=".claude/skills/frame0-wireframe/scripts/frame0-export-batch.sh"
|
||||
|
||||
# Dry run first — shows full manifest, no Frame0 calls
|
||||
$BATCH --dry-run
|
||||
|
||||
# Export everything (skips PNGs already newer than their JSON)
|
||||
$BATCH
|
||||
|
||||
# Export one category only
|
||||
$BATCH --category dialogue
|
||||
|
||||
# Force re-export of everything
|
||||
$BATCH --force
|
||||
```
|
||||
|
||||
**Always dry-run first, then get approval before running the live export.**
|
||||
|
||||
## Low-Level Commands
|
||||
|
||||
For ad-hoc operations or exec calls not covered by sync:
|
||||
|
||||
```bash
|
||||
CMD=".claude/skills/frame0-wireframe/scripts/frame0-cmd.sh"
|
||||
$CMD health
|
||||
$CMD list-pages
|
||||
$CMD current-page
|
||||
$CMD get-page <page-id>
|
||||
$CMD create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
||||
$CMD create-connector <tail-id> <head-id>
|
||||
$CMD move <shape-id> <dx> <dy>
|
||||
$CMD export --format image/png
|
||||
$CMD exec "view:fit-to-screen" '{}'
|
||||
```
|
||||
|
||||
If you find yourself using `exec` for the same command repeatedly, flag it as
|
||||
a candidate for a proper wrapper in `frame0-cmd.sh`.
|
||||
|
||||
## Project Styling Defaults
|
||||
|
||||
Colors from `docs/design/visual-grammar-v01.md`:
|
||||
|
||||
| Role | Hex | Frame0 token |
|
||||
|------|-----|-------------|
|
||||
| Background | `#1a1e24` | `$sage3` |
|
||||
| Stroke | `#333340` | `$slate6` |
|
||||
| Fill | `#2a3040` | `$slate5` |
|
||||
| Text | `#c8d0e0` | `$mint12` |
|
||||
| Accent | `#c8d8f0` | `$blue12` |
|
||||
|
||||
Use hex when authoring new wireframes. Frame0 maps them to theme tokens on push.
|
||||
|
||||
## Output Convention
|
||||
|
||||
```
|
||||
docs/design/wireframes/
|
||||
hud/ # HUD layout wireframes
|
||||
menus/ # Menu screen wireframes
|
||||
dialogue/ # Dialogue box wireframes
|
||||
insert/ # Neural insert wireframes
|
||||
```
|
||||
|
||||
Both `.json` source and `.png` exports are tracked in git.
|
||||
`.idmap.json` mapping files are gitignored.
|
||||
|
||||
## Agent Guidance
|
||||
|
||||
- **Araminta** — Primary user. Full wireframe creation, layout iteration,
|
||||
visual consistency. Uses all component library patterns.
|
||||
- **Tyre** — Interface architecture wireframes. System boundary diagrams.
|
||||
- **Qatux** — Export wireframes for UI decision records and documentation.
|
||||
|
||||
## References
|
||||
|
||||
- `references/component-library.md` — Pre-built JSON wireframe templates
|
||||
(HUD, dialogue, menus, modals, lists, inventory). Copy and adapt.
|
||||
- `references/api-reference.md` — Frame0 HTTP API command reference and
|
||||
type mappings. Read for low-level control.
|
||||
- `references/setup-guide.md` — Frame0 installation and startup for Fedora.
|
||||
@@ -0,0 +1,241 @@
|
||||
# Frame0 HTTP API Reference
|
||||
|
||||
Frame0 exposes a local HTTP API when the desktop app is running.
|
||||
|
||||
## Endpoint
|
||||
|
||||
```
|
||||
POST http://localhost:{port}/execute_command
|
||||
Content-Type: application/json
|
||||
```
|
||||
|
||||
Default port: **58320** (override via `FRAME0_PORT` env var or `--port` flag).
|
||||
|
||||
## Request / Response
|
||||
|
||||
```json
|
||||
{"command": "namespace:action", "args": { ... }}
|
||||
```
|
||||
|
||||
```json
|
||||
{"success": true, "data": { ... }}
|
||||
{"success": false, "error": "description"}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Type Mapping
|
||||
|
||||
Frame0 uses different type names for create vs get:
|
||||
|
||||
| Create API (`type`) | Get API (internal) | Description |
|
||||
|--------------------|--------------------|-------------|
|
||||
| `Rectangle` | `Box` | Rectangle with optional corners |
|
||||
| `Ellipse` | `Ellipse` | Circle/ellipse |
|
||||
| `Text` | `Text` | Text label |
|
||||
| `Line` | `Line` | Line/polyline |
|
||||
| `Frame` | `Frame` | Container from library |
|
||||
| `Freehand` | `Freehand` | Freehand drawing |
|
||||
| `Highlighter` | `Highlighter` | Highlighter stroke |
|
||||
|
||||
The sync script handles this mapping transparently.
|
||||
|
||||
## Color Tokens
|
||||
|
||||
Frame0 maps hex colors to theme tokens on creation (`convertColors: true`):
|
||||
|
||||
| Hex | Token | Role |
|
||||
|-----|-------|------|
|
||||
| `#1a1e24` | `$sage3` | Background |
|
||||
| `#2a3040` | `$slate5` | Fill |
|
||||
| `#333340` | `$slate6` | Stroke |
|
||||
| `#c8d0e0` | `$mint12` | Text |
|
||||
| `#c8d8f0` | `$blue12` | Accent |
|
||||
|
||||
Both hex and token strings work in the API. Tokens are preserved on round-trip.
|
||||
|
||||
---
|
||||
|
||||
## Commands
|
||||
|
||||
### shape:create-shape
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "shape:create-shape",
|
||||
"args": {
|
||||
"type": "Rectangle",
|
||||
"shapeProps": {
|
||||
"name": "my-button",
|
||||
"left": 100, "top": 200, "width": 120, "height": 36,
|
||||
"fillColor": "#2a3040",
|
||||
"strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"parentId": "optional-parent-shape-id",
|
||||
"convertColors": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Returns: shape ID (string).
|
||||
|
||||
### shape:get-shape
|
||||
|
||||
```json
|
||||
{"command": "shape:get-shape", "args": {"shapeId": "id"}}
|
||||
```
|
||||
|
||||
### shape:update-shape
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "shape:update-shape",
|
||||
"args": {
|
||||
"shapeId": "id",
|
||||
"shapeProps": {"fillColor": "#1a1e24", "text": "Updated"},
|
||||
"convertColors": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### shape:move
|
||||
|
||||
```json
|
||||
{"command": "shape:move", "args": {"shapeId": "id", "dx": 50, "dy": -20}}
|
||||
```
|
||||
|
||||
### shape:create-connector
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "shape:create-connector",
|
||||
"args": {
|
||||
"tailId": "source-id",
|
||||
"headId": "target-id",
|
||||
"shapeProps": {"strokeColor": "#c8d8f0"},
|
||||
"convertColors": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### shape:create-icon
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "shape:create-icon",
|
||||
"args": {
|
||||
"iconName": "search",
|
||||
"shapeProps": {"left": 100, "top": 100, "width": 24, "height": 24}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### shape:get-available-icons
|
||||
|
||||
```json
|
||||
{"command": "shape:get-available-icons", "args": {}}
|
||||
```
|
||||
|
||||
### shape:group / shape:ungroup
|
||||
|
||||
```json
|
||||
{"command": "shape:group", "args": {"shapeIdArray": ["id1", "id2"]}}
|
||||
{"command": "shape:ungroup", "args": {"shapeIdArray": ["group-id"]}}
|
||||
```
|
||||
|
||||
### edit:delete / edit:duplicate
|
||||
|
||||
```json
|
||||
{"command": "edit:delete", "args": {"shapeIdArray": ["id1", "id2"]}}
|
||||
{"command": "edit:duplicate", "args": {"shapeIdArray": ["id"], "dx": 20, "dy": 0}}
|
||||
```
|
||||
|
||||
### page:add
|
||||
|
||||
```json
|
||||
{"command": "page:add", "args": {"pageProps": {"name": "Page Name"}}}
|
||||
```
|
||||
|
||||
Returns: `{id, type, name}`.
|
||||
|
||||
### page:get
|
||||
|
||||
```json
|
||||
{"command": "page:get", "args": {"pageId": "id", "exportShapes": true}}
|
||||
```
|
||||
|
||||
### page:get-current-page
|
||||
|
||||
```json
|
||||
{"command": "page:get-current-page", "args": {}}
|
||||
```
|
||||
|
||||
Returns: page ID string.
|
||||
|
||||
### page:set-current-page
|
||||
|
||||
```json
|
||||
{"command": "page:set-current-page", "args": {"pageId": "id"}}
|
||||
```
|
||||
|
||||
### doc:get (list all pages)
|
||||
|
||||
```json
|
||||
{"command": "doc:get", "args": {"exportPages": true, "exportShapes": false}}
|
||||
```
|
||||
|
||||
### page:delete
|
||||
|
||||
```json
|
||||
{"command": "page:delete", "args": {"pageId": "id"}}
|
||||
```
|
||||
|
||||
### file:export-image
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "file:export-image",
|
||||
"args": {
|
||||
"pageId": "optional-page-id",
|
||||
"format": "image/png",
|
||||
"fillBackground": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Formats: `image/png`, `image/jpeg`, `image/webp`, `image/svg+xml`.
|
||||
Returns: base64-encoded image data.
|
||||
|
||||
### view:fit-to-screen
|
||||
|
||||
```json
|
||||
{"command": "view:fit-to-screen", "args": {}}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Shape Properties
|
||||
|
||||
| Property | Type | Notes |
|
||||
|----------|------|-------|
|
||||
| `name` | string | Shape identifier/label |
|
||||
| `left` | number | X position (origin: top-left) |
|
||||
| `top` | number | Y position |
|
||||
| `width` | number | Width in pixels |
|
||||
| `height` | number | Height in pixels |
|
||||
| `fillColor` | string | Hex or `$token` |
|
||||
| `strokeColor` | string | Hex or `$token` |
|
||||
| `strokeWidth` | number | Border width |
|
||||
| `fontColor` | string | Text color (hex or `$token`) |
|
||||
| `fontSize` | number | Font size in pixels |
|
||||
| `fontFamily` | string | Font name (Frame0 default: `Loranthus`) |
|
||||
| `text` | string | Text content |
|
||||
| `wordWrap` | boolean | Enable word wrapping |
|
||||
| `corners` | number[4] | Border radius [TL, TR, BR, BL] |
|
||||
| `roughness` | number | Sketch roughness (Frame0 default: 1) |
|
||||
| `constraints` | array | Auto-sizing constraints |
|
||||
| `horzAlign` | string | Horizontal text alignment |
|
||||
| `vertAlign` | string | Vertical text alignment |
|
||||
| `fillStyle` | string | Fill style (`none` for transparent) |
|
||||
| `path` | array | Coordinate pairs for lines |
|
||||
@@ -0,0 +1,467 @@
|
||||
# Component Library
|
||||
|
||||
Pre-built wireframe JSON templates for The Settled Reach UI. Copy the JSON,
|
||||
adapt positions/sizes, save to `docs/design/wireframes/{category}/`, and push.
|
||||
|
||||
**Viewport:** 1140x780 (Godot project settings)
|
||||
**Grid unit:** 8px
|
||||
**Min touch target:** 36px height
|
||||
**Font sizes:** 12 (label), 14 (body), 16 (subtitle), 18 (heading), 24 (title)
|
||||
|
||||
---
|
||||
|
||||
## 1. HUD Layout
|
||||
|
||||
Main gameplay overlay. Minimap top-right, monologue bottom-center,
|
||||
insert display bottom-left, action hints bottom-right.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "HUD Layout",
|
||||
"shapes": {
|
||||
"minimap": {
|
||||
"type": "Rectangle",
|
||||
"left": 880, "top": 20, "width": 240, "height": 240,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"minimap-label": {
|
||||
"type": "Text",
|
||||
"parent": "minimap",
|
||||
"left": 890, "top": 30,
|
||||
"text": "Minimap",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"monologue": {
|
||||
"type": "Rectangle",
|
||||
"left": 300, "top": 680, "width": 520, "height": 80,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"monologue-text": {
|
||||
"type": "Text",
|
||||
"parent": "monologue",
|
||||
"left": 310, "top": 700, "width": 500,
|
||||
"text": "Internal monologue text appears here...",
|
||||
"fontColor": "#c8d0e0", "fontSize": 13, "wordWrap": true
|
||||
},
|
||||
"insert": {
|
||||
"type": "Rectangle",
|
||||
"left": 20, "top": 600, "width": 260, "height": 160,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"insert-label": {
|
||||
"type": "Text",
|
||||
"parent": "insert",
|
||||
"left": 30, "top": 620,
|
||||
"text": "Neural Insert Data",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"hints": {
|
||||
"type": "Rectangle",
|
||||
"left": 880, "top": 700, "width": 240, "height": 60,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"hints-label": {
|
||||
"type": "Text",
|
||||
"parent": "hints",
|
||||
"left": 890, "top": 720,
|
||||
"text": "[E] Interact [TAB] Insert",
|
||||
"fontColor": "#c8d0e0", "fontSize": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 2. Dialogue Box
|
||||
|
||||
Speaker panel with response options. Anchored bottom-center during dialogue mode.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Dialogue Box",
|
||||
"shapes": {
|
||||
"panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 170, "top": 500, "width": 800, "height": 260,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [8, 8, 8, 8]
|
||||
},
|
||||
"speaker": {
|
||||
"type": "Text",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 520,
|
||||
"text": "LERA KONSTANTIN",
|
||||
"fontColor": "#c8d0e0", "fontSize": 16
|
||||
},
|
||||
"text-area": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 550, "width": 760, "height": 100,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"dialogue-text": {
|
||||
"type": "Text",
|
||||
"parent": "text-area",
|
||||
"left": 200, "top": 560, "width": 740,
|
||||
"text": "You look like you could use a drink. First time on the station?",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||
},
|
||||
"btn-option1": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 670, "width": 370, "height": 30,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-option1-label": {
|
||||
"type": "Text",
|
||||
"parent": "btn-option1",
|
||||
"left": 200, "top": 674,
|
||||
"text": "[1] Ask about the station",
|
||||
"fontColor": "#c8d8f0", "fontSize": 12
|
||||
},
|
||||
"btn-option2": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 190, "top": 710, "width": 370, "height": 30,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-option2-label": {
|
||||
"type": "Text",
|
||||
"parent": "btn-option2",
|
||||
"left": 200, "top": 714,
|
||||
"text": "[2] Ask about recent events",
|
||||
"fontColor": "#c8d8f0", "fontSize": 12
|
||||
},
|
||||
"btn-leave": {
|
||||
"type": "Rectangle",
|
||||
"parent": "panel",
|
||||
"left": 580, "top": 670, "width": 180, "height": 30,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-leave-label": {
|
||||
"type": "Text",
|
||||
"parent": "btn-leave",
|
||||
"left": 590, "top": 674,
|
||||
"text": "[3] Leave",
|
||||
"fontColor": "#c8d8f0", "fontSize": 12
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 3. Menu Screen
|
||||
|
||||
Full-screen menu with sidebar navigation and content area.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Pause Menu",
|
||||
"shapes": {
|
||||
"bg": {
|
||||
"type": "Rectangle",
|
||||
"left": 0, "top": 0, "width": 1140, "height": 780,
|
||||
"fillColor": "#1a1e24"
|
||||
},
|
||||
"nav": {
|
||||
"type": "Rectangle",
|
||||
"parent": "bg",
|
||||
"left": 20, "top": 20, "width": 200, "height": 740,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-inventory": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 40, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-inventory-label": {
|
||||
"type": "Text", "parent": "btn-inventory",
|
||||
"left": 40, "top": 48, "text": "Inventory",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-journal": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 86, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-journal-label": {
|
||||
"type": "Text", "parent": "btn-journal",
|
||||
"left": 40, "top": 94, "text": "Journal",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-map": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 132, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-map-label": {
|
||||
"type": "Text", "parent": "btn-map",
|
||||
"left": 40, "top": 140, "text": "Map",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-settings": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 178, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-settings-label": {
|
||||
"type": "Text", "parent": "btn-settings",
|
||||
"left": 40, "top": 186, "text": "Settings",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-resume": {
|
||||
"type": "Rectangle", "parent": "nav",
|
||||
"left": 30, "top": 720, "width": 180, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-resume-label": {
|
||||
"type": "Text", "parent": "btn-resume",
|
||||
"left": 40, "top": 728, "text": "Resume",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"content": {
|
||||
"type": "Rectangle",
|
||||
"parent": "bg",
|
||||
"left": 240, "top": 20, "width": 880, "height": 740,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340",
|
||||
"corners": [4, 4, 4, 4]
|
||||
},
|
||||
"content-label": {
|
||||
"type": "Text", "parent": "content",
|
||||
"left": 260, "top": 40,
|
||||
"text": "Content area",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 4. Modal Dialog
|
||||
|
||||
Centered overlay for confirmations, alerts, choices.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Modal Dialog",
|
||||
"shapes": {
|
||||
"overlay": {
|
||||
"type": "Rectangle",
|
||||
"left": 0, "top": 0, "width": 1140, "height": 780,
|
||||
"fillColor": "#0a0c10"
|
||||
},
|
||||
"modal": {
|
||||
"type": "Rectangle",
|
||||
"parent": "overlay",
|
||||
"left": 320, "top": 240, "width": 500, "height": 300,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340",
|
||||
"corners": [8, 8, 8, 8]
|
||||
},
|
||||
"title": {
|
||||
"type": "Text", "parent": "modal",
|
||||
"left": 340, "top": 260,
|
||||
"text": "Confirm Action",
|
||||
"fontColor": "#c8d0e0", "fontSize": 18
|
||||
},
|
||||
"divider": {
|
||||
"type": "Line", "parent": "modal",
|
||||
"left": 340, "top": 290, "width": 460, "height": 0,
|
||||
"strokeColor": "#333340"
|
||||
},
|
||||
"body-1": {
|
||||
"type": "Text", "parent": "modal",
|
||||
"left": 340, "top": 310,
|
||||
"text": "Are you sure you want to proceed?",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"body-2": {
|
||||
"type": "Text", "parent": "modal",
|
||||
"left": 340, "top": 340,
|
||||
"text": "This action cannot be undone.",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"btn-cancel": {
|
||||
"type": "Rectangle", "parent": "modal",
|
||||
"left": 480, "top": 480, "width": 120, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-cancel-label": {
|
||||
"type": "Text", "parent": "btn-cancel",
|
||||
"left": 510, "top": 488,
|
||||
"text": "Cancel",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"btn-confirm": {
|
||||
"type": "Rectangle", "parent": "modal",
|
||||
"left": 620, "top": 480, "width": 120, "height": 36,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"btn-confirm-label": {
|
||||
"type": "Text", "parent": "btn-confirm",
|
||||
"left": 645, "top": 488,
|
||||
"text": "Confirm",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 5. List View
|
||||
|
||||
Scrollable list with item selection and detail panel.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "List View",
|
||||
"shapes": {
|
||||
"list-panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 20, "top": 20, "width": 400, "height": 740,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"item-1": {
|
||||
"type": "Rectangle", "parent": "list-panel",
|
||||
"left": 30, "top": 30, "width": 380, "height": 40,
|
||||
"fillColor": "#2a3040", "strokeColor": "#c8d8f0", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"item-1-label": {
|
||||
"type": "Text", "parent": "item-1",
|
||||
"left": 40, "top": 38, "text": "Item Alpha",
|
||||
"fontColor": "#c8d8f0", "fontSize": 14
|
||||
},
|
||||
"item-2": {
|
||||
"type": "Rectangle", "parent": "list-panel",
|
||||
"left": 30, "top": 80, "width": 380, "height": 40,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"item-2-label": {
|
||||
"type": "Text", "parent": "item-2",
|
||||
"left": 40, "top": 88, "text": "Item Beta",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"item-3": {
|
||||
"type": "Rectangle", "parent": "list-panel",
|
||||
"left": 30, "top": 130, "width": 380, "height": 40,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"item-3-label": {
|
||||
"type": "Text", "parent": "item-3",
|
||||
"left": 40, "top": 138, "text": "Item Gamma",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14
|
||||
},
|
||||
"detail-panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 440, "top": 20, "width": 680, "height": 740,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"detail-title": {
|
||||
"type": "Text", "parent": "detail-panel",
|
||||
"left": 460, "top": 40,
|
||||
"text": "Item Alpha",
|
||||
"fontColor": "#c8d0e0", "fontSize": 18
|
||||
},
|
||||
"detail-body": {
|
||||
"type": "Text", "parent": "detail-panel",
|
||||
"left": 460, "top": 80, "width": 640,
|
||||
"text": "Description and properties appear here.",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 6. Inventory Grid
|
||||
|
||||
Grid of cells for item management.
|
||||
|
||||
```json
|
||||
{
|
||||
"name": "Inventory Grid",
|
||||
"shapes": {
|
||||
"panel": {
|
||||
"type": "Rectangle",
|
||||
"left": 240, "top": 100, "width": 660, "height": 580,
|
||||
"fillColor": "#1a1e24", "strokeColor": "#333340", "corners": [8, 8, 8, 8]
|
||||
},
|
||||
"title": {
|
||||
"type": "Text", "parent": "panel",
|
||||
"left": 260, "top": 120,
|
||||
"text": "INVENTORY",
|
||||
"fontColor": "#c8d0e0", "fontSize": 18
|
||||
},
|
||||
"cell-1-1": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 260, "top": 160, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-1-2": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 332, "top": 160, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-1-3": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 404, "top": 160, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-1-4": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 476, "top": 160, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-2-1": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 260, "top": 232, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-2-2": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 332, "top": 232, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-2-3": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 404, "top": 232, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"cell-2-4": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 476, "top": 232, "width": 64, "height": 64,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"detail": {
|
||||
"type": "Rectangle", "parent": "panel",
|
||||
"left": 580, "top": 160, "width": 300, "height": 400,
|
||||
"fillColor": "#2a3040", "strokeColor": "#333340", "corners": [4, 4, 4, 4]
|
||||
},
|
||||
"detail-title": {
|
||||
"type": "Text", "parent": "detail",
|
||||
"left": 600, "top": 180,
|
||||
"text": "Selected Item Name",
|
||||
"fontColor": "#c8d0e0", "fontSize": 16
|
||||
},
|
||||
"detail-body": {
|
||||
"type": "Text", "parent": "detail",
|
||||
"left": 600, "top": 210, "width": 260,
|
||||
"text": "Item description and stats",
|
||||
"fontColor": "#c8d0e0", "fontSize": 14, "wordWrap": true
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
@@ -0,0 +1,53 @@
|
||||
# Frame0 Setup Guide
|
||||
|
||||
## Installation (Fedora)
|
||||
|
||||
Download from https://frame0.app/download and install the RPM:
|
||||
|
||||
```bash
|
||||
sudo dnf install ./frame0-*.x86_64.rpm
|
||||
```
|
||||
|
||||
Requires: Fedora 40 or later (x86_64).
|
||||
|
||||
## Starting Frame0
|
||||
|
||||
Launch the desktop application:
|
||||
|
||||
```bash
|
||||
frame0 &
|
||||
```
|
||||
|
||||
Frame0 exposes an HTTP API at `localhost:58320` when running.
|
||||
|
||||
## Verify API Access
|
||||
|
||||
```bash
|
||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh health
|
||||
```
|
||||
|
||||
Expected output: `Frame0 is running on port 58320`
|
||||
|
||||
## Port Configuration
|
||||
|
||||
Default port: **58320**
|
||||
|
||||
To use a different port, set the environment variable:
|
||||
|
||||
```bash
|
||||
export FRAME0_PORT=58321
|
||||
```
|
||||
|
||||
Or pass `--port` to any script:
|
||||
|
||||
```bash
|
||||
.claude/skills/frame0-wireframe/scripts/frame0-cmd.sh --port 58321 health
|
||||
```
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
| Symptom | Cause | Fix |
|
||||
|---------|-------|-----|
|
||||
| "Connection refused" | Frame0 not running | Start the desktop app |
|
||||
| "Port in use" | Another instance running | Close duplicate or use different port |
|
||||
| Script hangs | API unresponsive | Restart Frame0 |
|
||||
+253
@@ -0,0 +1,253 @@
|
||||
#!/bin/bash
|
||||
set -euo pipefail
|
||||
|
||||
PORT="${FRAME0_PORT:-58320}"
|
||||
ENDPOINT="http://localhost:${PORT}/execute_command"
|
||||
|
||||
usage() {
|
||||
cat <<EOF
|
||||
Usage: $(basename "$0") <command> [args...] [--port N]
|
||||
|
||||
Low-level Frame0 HTTP API wrapper. Replaces the MCP server with direct
|
||||
curl calls. Requires Frame0 desktop app to be running.
|
||||
|
||||
Commands:
|
||||
health Check if Frame0 is running
|
||||
exec <namespace:action> <json> Execute a raw API command
|
||||
create-shape <type> <json-props> Create a shape (Rectangle, Ellipse, Text, Line)
|
||||
get-shape <id> Get shape details
|
||||
update-shape <id> <json-props> Update shape properties
|
||||
delete <id> [id...] Delete shapes by ID
|
||||
move <id> <dx> <dy> Move a shape by pixel offset
|
||||
duplicate <id> Duplicate a shape
|
||||
group <id> [id...] Group shapes
|
||||
ungroup <group-id> Ungroup a group
|
||||
create-connector <tail-id> <head-id> [json-props] Connect two shapes
|
||||
create-icon <name> <json-props> Create an icon shape
|
||||
add-page <name> Add a new page (becomes current)
|
||||
get-page [page-id] Get current or specific page data
|
||||
list-pages [--shapes] List all pages (--shapes for shape data)
|
||||
current-page Get current page ID
|
||||
set-page <page-id> Set current page
|
||||
export [page-id] [--format mime] Export page as image (default: image/png)
|
||||
fit Fit view to screen
|
||||
|
||||
Options:
|
||||
--port N Frame0 API port (default: $PORT, env: FRAME0_PORT)
|
||||
|
||||
Examples:
|
||||
$(basename "$0") health
|
||||
$(basename "$0") add-page "HUD Layout"
|
||||
$(basename "$0") create-shape Rectangle '{"name":"btn","left":100,"top":100,"width":120,"height":36}'
|
||||
$(basename "$0") list-pages
|
||||
$(basename "$0") export --format image/png
|
||||
EOF
|
||||
exit 1
|
||||
}
|
||||
|
||||
# Parse --port from anywhere in args
|
||||
ARGS=()
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--port) PORT="$2"; ENDPOINT="http://localhost:${PORT}/execute_command"; shift 2 ;;
|
||||
*) ARGS+=("$1"); shift ;;
|
||||
esac
|
||||
done
|
||||
set -- "${ARGS[@]+"${ARGS[@]}"}"
|
||||
|
||||
[[ $# -lt 1 ]] && usage
|
||||
|
||||
# Execute a Frame0 API command, return data or error
|
||||
frame0_exec() {
|
||||
local command="$1"
|
||||
local args
|
||||
args="${2:-"{}"}"
|
||||
|
||||
local response
|
||||
response=$(curl -s -w "\n%{http_code}" -X POST "$ENDPOINT" \
|
||||
-H "Content-Type: application/json" \
|
||||
-d "{\"command\": \"$command\", \"args\": $args}" 2>&1) || {
|
||||
echo "ERROR: Cannot connect to Frame0 at localhost:$PORT" >&2
|
||||
echo "Is Frame0 running? See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
||||
return 1
|
||||
}
|
||||
|
||||
local http_code body
|
||||
http_code=$(echo "$response" | tail -1)
|
||||
body=$(echo "$response" | sed '$d')
|
||||
|
||||
if [[ "$http_code" != 2* ]]; then
|
||||
echo "ERROR: HTTP $http_code from Frame0" >&2
|
||||
echo "$body" >&2
|
||||
return 1
|
||||
fi
|
||||
|
||||
# Parse success/error from response
|
||||
python3 -c "
|
||||
import sys, json
|
||||
try:
|
||||
r = json.loads(sys.stdin.read())
|
||||
if r.get('success'):
|
||||
d = r.get('data')
|
||||
if d is not None:
|
||||
print(json.dumps(d, indent=2))
|
||||
else:
|
||||
print('ERROR: ' + str(r.get('error', 'Unknown error')), file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except json.JSONDecodeError as e:
|
||||
print(f'ERROR: Invalid JSON response: {e}', file=sys.stderr)
|
||||
sys.exit(1)
|
||||
" <<< "$body"
|
||||
}
|
||||
|
||||
# Build JSON array from remaining args
|
||||
ids_to_json_array() {
|
||||
local arr="["
|
||||
local first=true
|
||||
for id in "$@"; do
|
||||
[[ "$first" == true ]] && first=false || arr+=","
|
||||
arr+="\"$id\""
|
||||
done
|
||||
arr+="]"
|
||||
echo "$arr"
|
||||
}
|
||||
|
||||
CMD="${1:-}"
|
||||
shift || true
|
||||
|
||||
case "$CMD" in
|
||||
health)
|
||||
if curl -s -o /dev/null -w "%{http_code}" "http://localhost:${PORT}/" 2>/dev/null | grep -q "^[23]"; then
|
||||
echo "Frame0 is running on port $PORT"
|
||||
else
|
||||
echo "Frame0 is NOT running on port $PORT" >&2
|
||||
echo "Start Frame0 desktop app, then retry." >&2
|
||||
echo "See: .claude/skills/frame0-wireframe/references/setup-guide.md" >&2
|
||||
exit 1
|
||||
fi
|
||||
;;
|
||||
|
||||
exec)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: exec <command> <json-args>" >&2; exit 1; }
|
||||
frame0_exec "$1" "$2"
|
||||
;;
|
||||
|
||||
create-shape)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: create-shape <Type> <json-props>" >&2; exit 1; }
|
||||
local_type="$1"
|
||||
local_props="$2"
|
||||
local_parent="${3:-}"
|
||||
local_parent_arg=""
|
||||
[[ -n "$local_parent" ]] && local_parent_arg=", \"parentId\": \"$local_parent\""
|
||||
frame0_exec "shape:create-shape" "{\"type\": \"$local_type\", \"shapeProps\": $local_props$local_parent_arg, \"convertColors\": true}"
|
||||
;;
|
||||
|
||||
get-shape)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: get-shape <id>" >&2; exit 1; }
|
||||
frame0_exec "shape:get-shape" "{\"shapeId\": \"$1\"}"
|
||||
;;
|
||||
|
||||
update-shape)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: update-shape <id> <json-props>" >&2; exit 1; }
|
||||
frame0_exec "shape:update-shape" "{\"shapeId\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
||||
;;
|
||||
|
||||
delete)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: delete <id> [id...]" >&2; exit 1; }
|
||||
local_arr=$(ids_to_json_array "$@")
|
||||
frame0_exec "edit:delete" "{\"shapeIdArray\": $local_arr}"
|
||||
;;
|
||||
|
||||
move)
|
||||
[[ $# -lt 3 ]] && { echo "Usage: move <id> <dx> <dy>" >&2; exit 1; }
|
||||
frame0_exec "shape:move" "{\"shapeId\": \"$1\", \"dx\": $2, \"dy\": $3}"
|
||||
;;
|
||||
|
||||
duplicate)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: duplicate <id> [dx] [dy]" >&2; exit 1; }
|
||||
local_dx="${2:-0}"
|
||||
local_dy="${3:-0}"
|
||||
frame0_exec "edit:duplicate" "{\"shapeIdArray\": [\"$1\"], \"dx\": $local_dx, \"dy\": $local_dy}"
|
||||
;;
|
||||
|
||||
group)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: group <id> <id> [id...]" >&2; exit 1; }
|
||||
local_arr=$(ids_to_json_array "$@")
|
||||
frame0_exec "shape:group" "{\"shapeIdArray\": $local_arr}"
|
||||
;;
|
||||
|
||||
ungroup)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: ungroup <group-id>" >&2; exit 1; }
|
||||
frame0_exec "shape:ungroup" "{\"shapeIdArray\": [\"$1\"]}"
|
||||
;;
|
||||
|
||||
create-connector)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: create-connector <tail-id> <head-id> [json-props]" >&2; exit 1; }
|
||||
local_props="${3:-{}}"
|
||||
frame0_exec "shape:create-connector" "{\"tailId\": \"$1\", \"headId\": \"$2\", \"shapeProps\": $local_props, \"convertColors\": true}"
|
||||
;;
|
||||
|
||||
create-icon)
|
||||
[[ $# -lt 2 ]] && { echo "Usage: create-icon <name> <json-props>" >&2; exit 1; }
|
||||
frame0_exec "shape:create-icon" "{\"iconName\": \"$1\", \"shapeProps\": $2, \"convertColors\": true}"
|
||||
;;
|
||||
|
||||
add-page)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: add-page <name>" >&2; exit 1; }
|
||||
frame0_exec "page:add" "{\"pageProps\": {\"name\": \"$1\"}}"
|
||||
;;
|
||||
|
||||
get-page)
|
||||
if [[ $# -ge 1 ]]; then
|
||||
frame0_exec "page:get" "{\"pageId\": \"$1\", \"exportShapes\": true}"
|
||||
else
|
||||
local_id
|
||||
local_id=$(frame0_exec "page:get-current-page")
|
||||
# Strip quotes from returned ID
|
||||
local_id=$(echo "$local_id" | tr -d '"')
|
||||
frame0_exec "page:get" "{\"pageId\": \"$local_id\", \"exportShapes\": true}"
|
||||
fi
|
||||
;;
|
||||
|
||||
list-pages)
|
||||
local_shapes="false"
|
||||
[[ "${1:-}" == "--shapes" ]] && local_shapes="true"
|
||||
frame0_exec "doc:get" "{\"exportPages\": true, \"exportShapes\": $local_shapes}"
|
||||
;;
|
||||
|
||||
current-page)
|
||||
frame0_exec "page:get-current-page"
|
||||
;;
|
||||
|
||||
set-page)
|
||||
[[ $# -lt 1 ]] && { echo "Usage: set-page <page-id>" >&2; exit 1; }
|
||||
frame0_exec "page:set-current-page" "{\"pageId\": \"$1\"}"
|
||||
;;
|
||||
|
||||
export)
|
||||
local_page_id=""
|
||||
local_format="image/png"
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--format) local_format="$2"; shift 2 ;;
|
||||
*) local_page_id="$1"; shift ;;
|
||||
esac
|
||||
done
|
||||
local_page_arg=""
|
||||
[[ -n "$local_page_id" ]] && local_page_arg="\"pageId\": \"$local_page_id\", "
|
||||
frame0_exec "file:export-image" "{${local_page_arg}\"format\": \"$local_format\", \"fillBackground\": true}"
|
||||
;;
|
||||
|
||||
fit)
|
||||
frame0_exec "view:fit-to-screen"
|
||||
;;
|
||||
|
||||
--help|-h|help)
|
||||
usage
|
||||
;;
|
||||
|
||||
*)
|
||||
echo "Unknown command: $CMD" >&2
|
||||
usage
|
||||
;;
|
||||
esac
|
||||
@@ -0,0 +1,159 @@
|
||||
#!/usr/bin/env bash
|
||||
# Batch export wireframes from JSON to PNG via Frame0.
|
||||
#
|
||||
# Finds all .json wireframe files under docs/design/wireframes/ and exports
|
||||
# each to a matching .png. Skips files whose PNG is already newer than the
|
||||
# JSON, unless --force is passed.
|
||||
#
|
||||
# Usage:
|
||||
# frame0-export-batch.sh [--dry-run] [--force] [--category CAT] [--root DIR]
|
||||
#
|
||||
# Options:
|
||||
# --dry-run Print manifest only, don't touch Frame0.
|
||||
# --force Re-export even if PNG already exists and is up to date.
|
||||
# --category CAT Limit to one subdirectory (e.g. --category dialogue)
|
||||
# --root DIR Wireframes root dir (default: docs/design/wireframes)
|
||||
#
|
||||
# Exit codes:
|
||||
# 0 All exports succeeded (or nothing to do)
|
||||
# 1 One or more exports failed
|
||||
|
||||
set -euo pipefail
|
||||
|
||||
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
||||
REPO_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)"
|
||||
SYNC="$SCRIPT_DIR/frame0-sync.py"
|
||||
DEFAULT_ROOT="$REPO_ROOT/docs/design/wireframes"
|
||||
|
||||
DRY_RUN=false
|
||||
FORCE=false
|
||||
CATEGORY=""
|
||||
WF_ROOT="$DEFAULT_ROOT"
|
||||
|
||||
# Parse arguments
|
||||
while [[ $# -gt 0 ]]; do
|
||||
case "$1" in
|
||||
--dry-run) DRY_RUN=true; shift ;;
|
||||
--force) FORCE=true; shift ;;
|
||||
--category) CATEGORY="$2"; shift 2 ;;
|
||||
--root) WF_ROOT="$2"; shift 2 ;;
|
||||
-h|--help)
|
||||
sed -n '/^# /p' "$0" | sed 's/^# //'
|
||||
exit 0
|
||||
;;
|
||||
*) echo "Unknown option: $1" >&2; exit 1 ;;
|
||||
esac
|
||||
done
|
||||
|
||||
if [[ ! -d "$WF_ROOT" ]]; then
|
||||
echo "ERROR: Wireframes directory not found: $WF_ROOT" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
# Collect JSON files, optionally filtered by category subdirectory
|
||||
mapfile -t JSON_FILES < <(
|
||||
if [[ -n "$CATEGORY" ]]; then
|
||||
find "$WF_ROOT/$CATEGORY" -name "*.json" ! -name ".*" | sort
|
||||
else
|
||||
find "$WF_ROOT" -name "*.json" ! -name ".*" | sort
|
||||
fi
|
||||
)
|
||||
|
||||
if [[ ${#JSON_FILES[@]} -eq 0 ]]; then
|
||||
echo "No wireframe JSON files found."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
# Classify files into to-export and to-skip
|
||||
TO_EXPORT=()
|
||||
TO_SKIP=()
|
||||
|
||||
for json in "${JSON_FILES[@]}"; do
|
||||
png="${json%.json}.png"
|
||||
if $FORCE || [[ ! -f "$png" ]] || [[ "$json" -nt "$png" ]]; then
|
||||
TO_EXPORT+=("$json")
|
||||
else
|
||||
TO_SKIP+=("$json")
|
||||
fi
|
||||
done
|
||||
|
||||
# Print manifest
|
||||
if [[ ${#TO_EXPORT[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Will export (${#TO_EXPORT[@]} files):"
|
||||
for json in "${TO_EXPORT[@]}"; do
|
||||
png="${json%.json}.png"
|
||||
rel="${json#$REPO_ROOT/}"
|
||||
if [[ ! -f "$png" ]]; then
|
||||
status="new"
|
||||
else
|
||||
status="updated"
|
||||
fi
|
||||
printf " [%-7s] %s\n" "$status" "$rel"
|
||||
done
|
||||
else
|
||||
echo ""
|
||||
echo "Nothing to export (all PNGs up to date)."
|
||||
fi
|
||||
|
||||
if [[ ${#TO_SKIP[@]} -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Will skip (${#TO_SKIP[@]} files already up to date):"
|
||||
for json in "${TO_SKIP[@]}"; do
|
||||
rel="${json#$REPO_ROOT/}"
|
||||
printf " [skip ] %s\n" "$rel"
|
||||
done
|
||||
fi
|
||||
|
||||
if $DRY_RUN; then
|
||||
echo ""
|
||||
echo "Dry run — no exports performed."
|
||||
exit 0
|
||||
fi
|
||||
|
||||
if [[ ${#TO_EXPORT[@]} -eq 0 ]]; then
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo ""
|
||||
PASSED=0
|
||||
FAILED=0
|
||||
FAILED_FILES=()
|
||||
|
||||
TOTAL=${#TO_EXPORT[@]}
|
||||
IDX=0
|
||||
|
||||
for json in "${TO_EXPORT[@]}"; do
|
||||
IDX=$((IDX + 1))
|
||||
png="${json%.json}.png"
|
||||
rel="${json#$REPO_ROOT/}"
|
||||
|
||||
printf "[%d/%d] %s ... " "$IDX" "$TOTAL" "$rel"
|
||||
|
||||
output=$(python3 "$SYNC" export "$json" "$png" 2>/tmp/frame0-batch-err.txt)
|
||||
rc=$?
|
||||
if [[ $rc -eq 0 ]]; then
|
||||
size=$(echo "$output" | tail -1 | grep -oP '\(\K[^)]+' || true)
|
||||
echo "ok $size"
|
||||
PASSED=$((PASSED + 1))
|
||||
else
|
||||
echo "FAILED"
|
||||
cat /tmp/frame0-batch-err.txt >&2
|
||||
FAILED=$((FAILED + 1))
|
||||
FAILED_FILES+=("$rel")
|
||||
fi
|
||||
done
|
||||
|
||||
echo ""
|
||||
echo "$PASSED exported, $FAILED failed."
|
||||
|
||||
if [[ $FAILED -gt 0 ]]; then
|
||||
echo ""
|
||||
echo "Failed:" >&2
|
||||
for f in "${FAILED_FILES[@]}"; do
|
||||
echo " $f" >&2
|
||||
done
|
||||
exit 1
|
||||
fi
|
||||
|
||||
exit 0
|
||||
+414
@@ -0,0 +1,414 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Frame0 sync: push local JSON wireframes to Frame0, pull pages back.
|
||||
|
||||
Local JSON is source of truth. Frame0 is a renderer.
|
||||
A mapping file tracks local_id <-> frame0_id across push/pull cycles.
|
||||
|
||||
Usage:
|
||||
frame0-sync.py push <wireframe.json> [--port PORT]
|
||||
frame0-sync.py pull <page-id|page-name> <output.json> [--port PORT]
|
||||
frame0-sync.py export <wireframe.json> <output.png> [--port PORT] [--format MIME]
|
||||
"""
|
||||
|
||||
import argparse
|
||||
import json
|
||||
import os
|
||||
import sys
|
||||
import urllib.request
|
||||
import urllib.error
|
||||
|
||||
DEFAULT_PORT = 58320
|
||||
|
||||
|
||||
def api(port, command, args=None):
|
||||
"""Execute a Frame0 API command. Returns the data field on success."""
|
||||
url = f"http://localhost:{port}/execute_command"
|
||||
payload = json.dumps({"command": command, "args": args or {}}).encode()
|
||||
req = urllib.request.Request(url, data=payload, headers={"Content-Type": "application/json"})
|
||||
try:
|
||||
with urllib.request.urlopen(req) as resp:
|
||||
result = json.loads(resp.read())
|
||||
except urllib.error.HTTPError as e:
|
||||
body = e.read().decode()
|
||||
print(f"ERROR: {command}: HTTP {e.code}: {body[:500]}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
except urllib.error.URLError as e:
|
||||
print(f"ERROR: Cannot connect to Frame0 on port {port}: {e}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
if not result.get("success"):
|
||||
print(f"ERROR: {command}: {result.get('error', 'unknown')}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
return result.get("data")
|
||||
|
||||
|
||||
# -- Mapping file management --------------------------------------------------
|
||||
|
||||
def mapping_path(wireframe_path):
|
||||
"""Derive the idmap path from the wireframe JSON path."""
|
||||
d = os.path.dirname(wireframe_path)
|
||||
base = os.path.splitext(os.path.basename(wireframe_path))[0]
|
||||
return os.path.join(d, f".{base}.idmap.json")
|
||||
|
||||
|
||||
def load_mapping(wireframe_path):
|
||||
p = mapping_path(wireframe_path)
|
||||
if os.path.exists(p):
|
||||
with open(p) as f:
|
||||
return json.load(f)
|
||||
return {"page_id": None, "shapes": {}, "connectors": {}}
|
||||
|
||||
|
||||
def save_mapping(wireframe_path, mapping):
|
||||
p = mapping_path(wireframe_path)
|
||||
os.makedirs(os.path.dirname(p) or ".", exist_ok=True)
|
||||
with open(p, "w") as f:
|
||||
json.dump(mapping, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
|
||||
# -- Push: local JSON -> Frame0 -----------------------------------------------
|
||||
|
||||
# Frame0 returns different type names from get vs what create accepts.
|
||||
# Map internal types back to create-API types.
|
||||
TYPE_TO_CREATE = {
|
||||
"Box": "Rectangle",
|
||||
"box": "Rectangle",
|
||||
}
|
||||
|
||||
|
||||
def topo_sort_shapes(shapes):
|
||||
"""Sort shape IDs so parents come before children."""
|
||||
order = []
|
||||
visited = set()
|
||||
|
||||
def visit(sid):
|
||||
if sid in visited:
|
||||
return
|
||||
visited.add(sid)
|
||||
shape = shapes[sid]
|
||||
parent = shape.get("parent")
|
||||
if parent and parent in shapes:
|
||||
visit(parent)
|
||||
order.append(sid)
|
||||
|
||||
for sid in shapes:
|
||||
visit(sid)
|
||||
return order
|
||||
|
||||
|
||||
def find_or_create_page(port, name, mapping):
|
||||
"""Find existing page by mapping or name, or create a new one."""
|
||||
# Try mapped page_id first
|
||||
if mapping.get("page_id"):
|
||||
try:
|
||||
page = api(port, "page:get", {"pageId": mapping["page_id"]})
|
||||
if page:
|
||||
return mapping["page_id"]
|
||||
except SystemExit:
|
||||
pass # Page no longer exists, fall through
|
||||
|
||||
# Search by name in existing pages
|
||||
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
||||
if doc and "children" in doc:
|
||||
for page in doc["children"]:
|
||||
if page.get("name") == name:
|
||||
return page["id"]
|
||||
|
||||
# Create new page
|
||||
page = api(port, "page:add", {"pageProps": {"name": name}})
|
||||
return page["id"]
|
||||
|
||||
|
||||
def clear_page(port, page_id):
|
||||
"""Delete all shapes on a page."""
|
||||
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
||||
if not page or "children" not in page:
|
||||
return
|
||||
shape_ids = [s["id"] for s in page["children"]]
|
||||
if shape_ids:
|
||||
api(port, "edit:delete", {"shapeIdArray": shape_ids})
|
||||
|
||||
|
||||
def push(wireframe_path, port):
|
||||
"""Push local wireframe JSON to Frame0."""
|
||||
with open(wireframe_path) as f:
|
||||
wireframe = json.load(f)
|
||||
|
||||
name = wireframe.get("name", os.path.splitext(os.path.basename(wireframe_path))[0])
|
||||
shapes = wireframe.get("shapes", {})
|
||||
connectors = wireframe.get("connectors", {})
|
||||
|
||||
mapping = load_mapping(wireframe_path)
|
||||
|
||||
# Find or create page
|
||||
page_id = find_or_create_page(port, name, mapping)
|
||||
mapping["page_id"] = page_id
|
||||
|
||||
# Switch to page and clear it
|
||||
api(port, "page:set-current-page", {"pageId": page_id})
|
||||
clear_page(port, page_id)
|
||||
|
||||
# Reset ID mappings (shapes are recreated)
|
||||
mapping["shapes"] = {}
|
||||
mapping["connectors"] = {}
|
||||
|
||||
# Create shapes in dependency order
|
||||
order = topo_sort_shapes(shapes)
|
||||
for local_id in order:
|
||||
shape = shapes[local_id]
|
||||
raw_type = shape.get("type", "Rectangle")
|
||||
shape_type = TYPE_TO_CREATE.get(raw_type, raw_type)
|
||||
parent_local = shape.get("parent")
|
||||
|
||||
# Build shapeProps from everything except meta fields
|
||||
meta_keys = {"type", "parent"}
|
||||
props = {k: v for k, v in shape.items() if k not in meta_keys}
|
||||
|
||||
# Set name to local_id if not explicitly set
|
||||
if "name" not in props:
|
||||
props["name"] = local_id
|
||||
|
||||
create_args = {
|
||||
"type": shape_type,
|
||||
"shapeProps": props,
|
||||
"convertColors": True,
|
||||
}
|
||||
|
||||
# Resolve parent ID
|
||||
if parent_local and parent_local in mapping["shapes"]:
|
||||
create_args["parentId"] = mapping["shapes"][parent_local]
|
||||
|
||||
f0_id = api(port, "shape:create-shape", create_args)
|
||||
mapping["shapes"][local_id] = f0_id
|
||||
|
||||
# Create connectors
|
||||
for local_id, conn in connectors.items():
|
||||
tail_local = conn.get("tailId")
|
||||
head_local = conn.get("headId")
|
||||
|
||||
if tail_local not in mapping["shapes"] or head_local not in mapping["shapes"]:
|
||||
print(f"WARNING: connector '{local_id}' references unknown shape, skipping", file=sys.stderr)
|
||||
continue
|
||||
|
||||
meta_keys = {"tailId", "headId"}
|
||||
props = {k: v for k, v in conn.items() if k not in meta_keys}
|
||||
if "name" not in props:
|
||||
props["name"] = local_id
|
||||
|
||||
f0_id = api(port, "shape:create-connector", {
|
||||
"tailId": mapping["shapes"][tail_local],
|
||||
"headId": mapping["shapes"][head_local],
|
||||
"shapeProps": props,
|
||||
"convertColors": True,
|
||||
})
|
||||
mapping["connectors"][local_id] = f0_id
|
||||
|
||||
# Fit to screen
|
||||
api(port, "view:fit-to-screen")
|
||||
|
||||
save_mapping(wireframe_path, mapping)
|
||||
total = len(mapping["shapes"]) + len(mapping["connectors"])
|
||||
print(f"Pushed '{name}' to Frame0: {len(mapping['shapes'])} shapes, {len(mapping['connectors'])} connectors")
|
||||
|
||||
|
||||
# -- Pull: Frame0 -> local JSON -----------------------------------------------
|
||||
|
||||
def pull(page_ref, output_path, port):
|
||||
"""Pull a Frame0 page into local wireframe JSON."""
|
||||
# Resolve page_ref: could be an ID or a name
|
||||
page_id = None
|
||||
doc = api(port, "doc:get", {"exportPages": True, "exportShapes": False})
|
||||
if doc and "children" in doc:
|
||||
for page in doc["children"]:
|
||||
if page["id"] == page_ref or page.get("name") == page_ref:
|
||||
page_id = page["id"]
|
||||
page_name = page.get("name", page_ref)
|
||||
break
|
||||
|
||||
if not page_id:
|
||||
print(f"ERROR: Page not found: {page_ref}", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
# Load existing mapping for reverse lookup
|
||||
mapping = load_mapping(output_path)
|
||||
reverse_map = {v: k for k, v in mapping.get("shapes", {}).items()}
|
||||
reverse_conn = {v: k for k, v in mapping.get("connectors", {}).items()}
|
||||
|
||||
# Get full page with shapes
|
||||
page = api(port, "page:get", {"pageId": page_id, "exportShapes": True})
|
||||
|
||||
shapes = {}
|
||||
connectors = {}
|
||||
new_mapping = {"page_id": page_id, "shapes": {}, "connectors": {}}
|
||||
auto_id_counter = [0]
|
||||
|
||||
def auto_id(f0_shape):
|
||||
"""Generate a stable local ID from shape name or auto-number."""
|
||||
# Prefer existing mapping
|
||||
f0_id = f0_shape["id"]
|
||||
if f0_id in reverse_map:
|
||||
return reverse_map[f0_id]
|
||||
# Use sanitized name
|
||||
name = f0_shape.get("name", "")
|
||||
if name:
|
||||
sanitized = name.lower().replace(" ", "-").replace("_", "-")
|
||||
if sanitized not in shapes:
|
||||
return sanitized
|
||||
# Fallback: auto-number
|
||||
auto_id_counter[0] += 1
|
||||
return f"s{auto_id_counter[0]:03d}"
|
||||
|
||||
def process_shape(f0_shape, parent_local_id=None):
|
||||
f0_id = f0_shape["id"]
|
||||
local_id = auto_id(f0_shape)
|
||||
new_mapping["shapes"][local_id] = f0_id
|
||||
|
||||
# Extract shape properties — only strip structural keys that our
|
||||
# ID mapping replaces. Everything else passes through as-is so the
|
||||
# local JSON speaks Frame0's native vocabulary.
|
||||
shape_type = f0_shape.get("type", "Box")
|
||||
skip_keys = {"id", "type", "children", "pageId", "parentId"}
|
||||
props = {k: v for k, v in f0_shape.items() if k not in skip_keys and v is not None}
|
||||
|
||||
entry = {"type": shape_type}
|
||||
if parent_local_id:
|
||||
entry["parent"] = parent_local_id
|
||||
entry.update(props)
|
||||
|
||||
# Remove name if it matches local_id (redundant)
|
||||
if entry.get("name") == local_id:
|
||||
del entry["name"]
|
||||
|
||||
shapes[local_id] = entry
|
||||
|
||||
# Process children recursively
|
||||
for child in f0_shape.get("children", []):
|
||||
child_type = child.get("type", "")
|
||||
if child_type == "Connector":
|
||||
process_connector(child)
|
||||
else:
|
||||
process_shape(child, local_id)
|
||||
|
||||
def process_connector(f0_conn):
|
||||
f0_id = f0_conn["id"]
|
||||
local_id = reverse_conn.get(f0_id)
|
||||
if not local_id:
|
||||
auto_id_counter[0] += 1
|
||||
local_id = f"c{auto_id_counter[0]:03d}"
|
||||
|
||||
new_mapping["connectors"][local_id] = f0_id
|
||||
|
||||
tail_f0 = f0_conn.get("tail", {}).get("id")
|
||||
head_f0 = f0_conn.get("head", {}).get("id")
|
||||
|
||||
entry = {}
|
||||
if tail_f0:
|
||||
# Will be resolved after all shapes are processed
|
||||
entry["_tailF0"] = tail_f0
|
||||
if head_f0:
|
||||
entry["_headF0"] = head_f0
|
||||
|
||||
skip_keys = {"id", "type", "children", "pageId", "tail", "head"}
|
||||
props = {k: v for k, v in f0_conn.items() if k not in skip_keys and v is not None}
|
||||
entry.update(props)
|
||||
|
||||
connectors[local_id] = entry
|
||||
|
||||
# Process all top-level shapes
|
||||
for child in page.get("children", []):
|
||||
child_type = child.get("type", "")
|
||||
if child_type == "Connector":
|
||||
process_connector(child)
|
||||
else:
|
||||
process_shape(child)
|
||||
|
||||
# Resolve connector references to local IDs
|
||||
f0_to_local = {v: k for k, v in new_mapping["shapes"].items()}
|
||||
for conn in connectors.values():
|
||||
tail_f0 = conn.pop("_tailF0", None)
|
||||
head_f0 = conn.pop("_headF0", None)
|
||||
if tail_f0 and tail_f0 in f0_to_local:
|
||||
conn["tailId"] = f0_to_local[tail_f0]
|
||||
if head_f0 and head_f0 in f0_to_local:
|
||||
conn["headId"] = f0_to_local[head_f0]
|
||||
|
||||
wireframe = {"name": page_name}
|
||||
if shapes:
|
||||
wireframe["shapes"] = shapes
|
||||
if connectors:
|
||||
wireframe["connectors"] = connectors
|
||||
|
||||
os.makedirs(os.path.dirname(output_path) or ".", exist_ok=True)
|
||||
with open(output_path, "w") as f:
|
||||
json.dump(wireframe, f, indent=2)
|
||||
f.write("\n")
|
||||
|
||||
save_mapping(output_path, new_mapping)
|
||||
print(f"Pulled '{page_name}' -> {output_path}: {len(shapes)} shapes, {len(connectors)} connectors")
|
||||
|
||||
|
||||
# -- Export: push then export as image -----------------------------------------
|
||||
|
||||
def export_image(wireframe_path, output_path, port, fmt="image/png"):
|
||||
"""Push wireframe to Frame0 and export the page as an image."""
|
||||
import base64
|
||||
|
||||
# Push first to ensure Frame0 is up to date
|
||||
push(wireframe_path, port)
|
||||
|
||||
mapping = load_mapping(wireframe_path)
|
||||
page_id = mapping.get("page_id")
|
||||
if not page_id:
|
||||
print("ERROR: No page_id in mapping after push", file=sys.stderr)
|
||||
sys.exit(1)
|
||||
|
||||
image_b64 = api(port, "file:export-image", {
|
||||
"pageId": page_id,
|
||||
"format": fmt,
|
||||
"fillBackground": True,
|
||||
})
|
||||
|
||||
image_bytes = base64.b64decode(image_b64)
|
||||
with open(output_path, "wb") as f:
|
||||
f.write(image_bytes)
|
||||
|
||||
print(f"Exported: {output_path} ({len(image_bytes) // 1024}KB)")
|
||||
|
||||
|
||||
# -- CLI -----------------------------------------------------------------------
|
||||
|
||||
def main():
|
||||
parser = argparse.ArgumentParser(description="Sync wireframe JSON with Frame0")
|
||||
parser.add_argument("--port", type=int, default=int(os.environ.get("FRAME0_PORT", DEFAULT_PORT)))
|
||||
sub = parser.add_subparsers(dest="command")
|
||||
|
||||
p_push = sub.add_parser("push", help="Push local JSON to Frame0")
|
||||
p_push.add_argument("wireframe", help="Path to wireframe .json file")
|
||||
|
||||
p_pull = sub.add_parser("pull", help="Pull Frame0 page to local JSON")
|
||||
p_pull.add_argument("page", help="Page ID or page name")
|
||||
p_pull.add_argument("output", help="Output .json path")
|
||||
|
||||
p_export = sub.add_parser("export", help="Push and export as image")
|
||||
p_export.add_argument("wireframe", help="Path to wireframe .json file")
|
||||
p_export.add_argument("output", help="Output image path (e.g. wireframe.png)")
|
||||
p_export.add_argument("--format", default="image/png",
|
||||
help="Export MIME type (default: image/png)")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
if args.command == "push":
|
||||
push(args.wireframe, args.port)
|
||||
elif args.command == "pull":
|
||||
pull(args.page, args.output, args.port)
|
||||
elif args.command == "export":
|
||||
export_image(args.wireframe, args.output, args.port, args.format)
|
||||
else:
|
||||
parser.print_help()
|
||||
sys.exit(1)
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@@ -0,0 +1,147 @@
|
||||
---
|
||||
name: git-commit
|
||||
description: >
|
||||
Git commit conventions for this repo — message style, changelog and
|
||||
version-bump rules, attribution trailer, and the safety rules that
|
||||
always apply. Use whenever the user says "commit", "commit this",
|
||||
"create a commit", "make a commit", "amend", or asks Claude to check
|
||||
work into git in any form.
|
||||
---
|
||||
|
||||
# Git commit rule — this repo
|
||||
|
||||
Follow these conventions whenever you create a commit in this repository. These are in addition to the standard Claude Code git-safety protocol (no `--no-verify`, no force-push to main, prefer new commits over `--amend`, etc.).
|
||||
|
||||
## Message style
|
||||
|
||||
- **First line:** imperative mood, ≤ 70 characters. Examples: `add sidecar PTY scaffold`, `fix IPC reconnect after app reload`, `update CLI exit-code contract`.
|
||||
- **Body (optional):** wrap at ~72 chars. Explain the *why* — the reason this change exists. The diff already shows the *what*; don't restate it in prose.
|
||||
- **No emojis.** Anywhere.
|
||||
- **Don't prefix with types** like `feat:` or `fix:` — this repo isn't Conventional Commits. (The Python-era clide under `legacy/` used Conventional Commits; the Flutter rebuild at the repo root does not.)
|
||||
- **Don't reference the current task or flow** (`for the v2.0 milestone`, `used by the canvas panel`) — that context belongs in the PR description and rots as the repo evolves.
|
||||
- **Naming:** the project is `clide`. The Flutter desktop app lives at the repo root; the Go sidecar/CLI binary is `clide`. The supporter project is `pql` (referenced, not part of this repo). The archived Python implementation lives under `legacy/`.
|
||||
|
||||
## Logically-separated commits
|
||||
|
||||
Default to **one logical change per commit**, even when a lot of work lands in the tree at once. When there's a pile of uncommitted or untracked files:
|
||||
|
||||
1. **Read `git status` + `git diff` first** — never stage the whole tree blind.
|
||||
2. **Group by concern**, not by file location. Typical concerns to separate:
|
||||
- **Bookkeeping** — `.gitignore`, editor/IDE config, lockfiles, `go.sum` / `pubspec.lock` updates.
|
||||
- **Documentation** — `README.md`, `CLAUDE.md`, ADRs under `docs/ADRs/`, design notes under `docs/`.
|
||||
- **General-purpose tooling / skills** — things that aren't project-specific (reusable skills, shared scripts).
|
||||
- **Project-specific conventions** — this repo's own rules.
|
||||
- **Feature or subsystem** — one cohesive change per commit; a sidecar change and an app change for the same feature can land together, but two unrelated features should split.
|
||||
- **Layer changes** — Flutter app, sidecar CLI, sidecar daemon, IPC server, pql wrapper, canvas driver, git panel — separate concerns; prefer separate commits when the changes are independent.
|
||||
3. **Sequence the commits** so each one is cleanly scoped, but don't obsess about whether each intermediate commit "works" — for scaffolding PRs it's fine if the full picture only snaps together at the end.
|
||||
4. **Prefer many small focused commits over one large mixed one** — a reviewer can read, revert, or cherry-pick a focused commit; they can't do any of those to a blob.
|
||||
5. **Use `git add <specific paths>`** — never `git add -A` or `git add .` when splitting, or you'll sweep in the next commit's work by accident.
|
||||
6. **Verify between commits** with `git status` and `git log -1` to confirm the split landed as intended.
|
||||
|
||||
Corollary: if a commit's subject line needs the word "and" to be accurate, it probably should have been two commits.
|
||||
|
||||
## Changelog discipline
|
||||
|
||||
This repo follows [Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). **Every user-visible commit must touch `CHANGELOG.md`** under the `## [Unreleased]` section, in the appropriate subsection:
|
||||
|
||||
- **Added** — new features or capabilities.
|
||||
- **Changed** — changes to existing functionality.
|
||||
- **Deprecated** — soon-to-be-removed features.
|
||||
- **Removed** — now-removed features.
|
||||
- **Fixed** — bug fixes.
|
||||
- **Security** — vulnerability fixes.
|
||||
|
||||
Entries should be short imperative phrases that describe user-facing impact — not implementation detail. "Added sidecar PTY support for terminal pane" beats "Added `internal/pty/session.go`."
|
||||
|
||||
### Be concise — this is the rule, not a suggestion
|
||||
|
||||
CHANGELOG entries must be **one or two short sentences**. Hard cap: **60 words per bullet**, enforced by the pre-push gate — verify before committing with `make changelog-gate` (run the `make` target, not the script it wraps). Aim for 30 or under; if you can't say it in one line wrapped at ~75 columns, you're writing the wrong document.
|
||||
|
||||
The CHANGELOG is read by humans scanning for what changed between two versions. It is **not** the place for the rationale, the probe results, the implementation detail, the behavior-change deep dive, or the "see also" cross-references. Those belong in:
|
||||
|
||||
- the **commit message body** — explain *why*, list evidence, name the trade-offs;
|
||||
- a **D-record / decision document** — durable architectural rationale;
|
||||
- the **ticket / PR description** — work-context and review notes.
|
||||
|
||||
Hard rules:
|
||||
|
||||
- **No multi-paragraph bullets.** One paragraph max. If you reach for a blank line inside a bullet, stop and split or trim.
|
||||
- **No "Behavior change:" / "Side benefit:" / "Note:" sub-headers inside a bullet.** Those are essay structure; put them in the commit message.
|
||||
- **No probe numbers, latency stats, or %-coverage deltas in entries.** ("hit 95% target" is fine; "0 hangs in 300 spawns vs ~5% before" is commit-body material.)
|
||||
- **No nested function/file lists inside parentheses.** If you find yourself writing `(foo, bar, baz, …)` for more than 3 items, just say "several X" and trust the diff.
|
||||
- **Don't restate the title in the body.** A bullet is its own title.
|
||||
|
||||
Calibration — match the **existing entries** in `CHANGELOG.md`. Open it, look at five recent bullets, write to that length. If your draft is visibly bigger than its neighbors, trim until it isn't.
|
||||
|
||||
Good:
|
||||
> - Mouse wheel scrolling in Claude pane — converts scroll events to PgUp/PgDown so TUI apps scroll naturally.
|
||||
|
||||
Bad (verbose; commit-body material leaked in):
|
||||
> - **PTY spawning switched from `forkpty()` to `posix_openpt()` + `posix_spawn()`** (T-96). `forkpty` calls `fork()` underneath, which is unsafe in the multithreaded Dart VM: about 5% of spawns deadlocked in the child before `execve` because libc locks held by ghost-threads remained "locked forever" in the forked child. `posix_spawn` uses `vfork` (glibc/musl/macOS), keeping the parent suspended until `execve` completes — no Dart code runs in the child. Probed: zero hangs in 300 sequential spawns vs ~5% before. **Behavior change:** missing executable / missing workingDirectory now surface as a `PtyException`…
|
||||
|
||||
Better:
|
||||
> - PTY spawning uses `posix_openpt` + `posix_spawn` instead of `forkpty` — closes a ~5% deadlock window in the multithreaded Dart VM (T-96). Missing exe/cwd now throw `PtyException` at spawn time.
|
||||
|
||||
**What skips the changelog:** pure bookkeeping commits that have no user-visible effect (typo fix in internal comment, `.gitignore` tweak, lint config change, reformatting). When in doubt, add an entry — the harm of an extra line is zero.
|
||||
|
||||
When a commit spans multiple entries (e.g. a feature that adds one thing and fixes another), add a line under each applicable subsection rather than cramming both into one.
|
||||
|
||||
## Cutting a release
|
||||
|
||||
Cutting a release is its own commit. In a single commit:
|
||||
|
||||
1. Move all entries from `## [Unreleased]` under a new heading `## [X.Y.Z] — YYYY-MM-DD`.
|
||||
2. Leave an empty `## [Unreleased]` section at the top with its subsection skeleton ready.
|
||||
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (drop the `-dev` suffix for the tag; re-add it on the next development commit if desired).
|
||||
4. Run `make gen-build-info` so `assets/licenses.yaml` `self.version:` re-syncs from pubspec (it's auto-rewritten by every build but commit the fresh state). Stage the resulting diff alongside step 3.
|
||||
5. Commit subject: `release vX.Y.Z`.
|
||||
|
||||
`pubspec.yaml` is the single source of truth for the version. Every `make` build/run/test target regenerates `lib/src/build_info.g.dart` (gitignored) and rewrites `assets/licenses.yaml` `self.version:` from it — so the Flutter app sees the current version everywhere without manual sync. Bumping `pubspec.yaml` and the changelog out of sync is the mistake this rule prevents.
|
||||
|
||||
## Attribution trailer
|
||||
|
||||
Every commit ends with the Claude co-author line:
|
||||
|
||||
```
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
```
|
||||
|
||||
The model-identifier variant produced by the Claude Code harness (e.g. `Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>`) is also accepted — don't rewrite it if the harness emits that form.
|
||||
|
||||
## HEREDOC discipline
|
||||
|
||||
Pass commit messages via HEREDOC so multi-line formatting survives:
|
||||
|
||||
```bash
|
||||
git commit -m "$(cat <<'EOF'
|
||||
short imperative summary
|
||||
|
||||
Optional longer body explaining why this change was needed,
|
||||
wrapped at about 72 characters.
|
||||
|
||||
Co-Authored-By: Claude <noreply@anthropic.com>
|
||||
EOF
|
||||
)"
|
||||
```
|
||||
|
||||
Never pass multi-line messages via `-m "line1\nline2"` or multiple `-m` flags — Git's behavior differs between shells and quoting regimes and the trailer can end up in the wrong place.
|
||||
|
||||
## What not to commit
|
||||
|
||||
- `.env` and any `*.env.local` — see `.gitignore`.
|
||||
- `.claude/settings.local.json` — user-specific Claude Code settings, ignored.
|
||||
- Build artefacts: `sidecar/bin/`, `sidecar/dist/`, `build/` (Flutter output), `app/.dart_tool/` — gitignored.
|
||||
- SQLite index files (`*.sqlite`, `*.sqlite-wal`, `*.sqlite-shm`, `*.db`) — caches generated against local repos; must never land here. Gitignored defensively.
|
||||
- Coverage / test output (`*.out`, `coverage.*`, `*.test`) — gitignored.
|
||||
|
||||
## Don't hand-manage `.pql/changelog`
|
||||
|
||||
The pre-commit hook exports the pql ticket DB and **auto-stages `.pql/changelog/` on every commit**. Don't `git add .pql/changelog` yourself and don't write a dedicated "flush the export" commit — just make your normal commit and the hook sweeps the ticket state in. The only thing to remember: a turn that files/changes a ticket but makes **zero commits** never fires the hook, so the change won't persist (and a later branch switch can drop it). The fix is simply to make a commit — you don't need to touch `.pql/changelog`.
|
||||
|
||||
## Safety reminders (reinforced from the global Claude Code protocol)
|
||||
|
||||
- **Never** `--no-verify`. If a pre-commit hook fails, fix the underlying issue and create a new commit.
|
||||
- **Never** `--amend` a commit unless the user explicitly asks. A failed-hook commit didn't land, so amending would overwrite the *previous* commit and lose work.
|
||||
- **Never** force-push to `main` or `master`. Warn the user if they ask.
|
||||
- When staging, prefer naming specific files over `git add -A` / `git add .` — those can sweep in secrets or unintended files.
|
||||
- `git status` before staging, `git diff --staged` before committing, `git log -1` after committing to confirm.
|
||||
@@ -0,0 +1,104 @@
|
||||
---
|
||||
name: penpot-login
|
||||
description: Connect to Penpot MCP - starts the MCP server, logs into penpot.schweitz.net, and installs/connects the plugin
|
||||
allowed-tools: Read, Bash, mcp__chrome-devtools__new_page, mcp__chrome-devtools__navigate_page, mcp__chrome-devtools__take_snapshot, mcp__chrome-devtools__take_screenshot, mcp__chrome-devtools__click, mcp__chrome-devtools__fill, mcp__chrome-devtools__press_key, mcp__chrome-devtools__list_pages
|
||||
---
|
||||
|
||||
# Penpot MCP Connection
|
||||
|
||||
This skill connects Claude to Penpot by:
|
||||
1. Starting the Penpot MCP server (if not running)
|
||||
2. Logging into penpot.schweitz.net via Authentik
|
||||
3. Installing and connecting the Penpot MCP plugin
|
||||
|
||||
## Step 1: Start MCP Server
|
||||
|
||||
Check if the MCP server is running, start it if not:
|
||||
|
||||
```bash
|
||||
# Check if server is running
|
||||
if ! curl -s http://localhost:9880/mcp > /dev/null 2>&1; then
|
||||
# Start the server in background
|
||||
cd ~/Projects/penpot-mcp && ./start-server.sh &
|
||||
sleep 3 # Wait for server to start
|
||||
fi
|
||||
```
|
||||
|
||||
Server endpoints when running:
|
||||
- MCP Server: http://localhost:9880/mcp
|
||||
- Plugin Server: http://localhost:9879/
|
||||
- WebSocket: ws://localhost:4402
|
||||
|
||||
## Step 2: Login Credentials
|
||||
|
||||
Read credentials from the project root file `claude-authentik-credentials.md`:
|
||||
!`cat claude-authentik-credentials.md`
|
||||
|
||||
## Step 3: Login Flow
|
||||
|
||||
1. **Navigate to Penpot**
|
||||
- Use `mcp__chrome-devtools__new_page` to go to `https://penpot.schweitz.net`
|
||||
|
||||
2. **Check if already logged in**
|
||||
- Take a snapshot
|
||||
- If you see "Projects" heading, you're logged in - skip to Step 4
|
||||
- If you see login page, continue with authentication
|
||||
|
||||
3. **Authenticate via Authentik** (if not logged in)
|
||||
- Click the OpenID button to redirect to Authentik
|
||||
- On auth.schweitz.net, fill username textbox
|
||||
- Click "Log in" button
|
||||
- Fill password textbox
|
||||
- Click "Continue" button
|
||||
- If redirected to Authentik user page instead of Penpot, navigate back to `https://penpot.schweitz.net`
|
||||
|
||||
## Step 4: Open Design File
|
||||
|
||||
1. **Navigate to the Clide project**
|
||||
- Double-click on the TUI file to open the workspace
|
||||
|
||||
## Step 5: Install/Connect Plugin
|
||||
|
||||
1. **Open Plugins menu**
|
||||
- Click the Plugins button (puzzle icon, keyboard shortcut: Cmd+Alt+P)
|
||||
|
||||
2. **Check if plugin is installed**
|
||||
- If "Penpot MCP Plugin" appears under "INSTALLED PLUGINS", click OPEN
|
||||
- Otherwise, install it first:
|
||||
|
||||
3. **Install plugin** (if not installed)
|
||||
- Fill the plugin URL textbox with: `http://localhost:9879/manifest.json`
|
||||
- Click INSTALL
|
||||
- Click ALLOW on the permissions dialog
|
||||
|
||||
4. **Connect to MCP server**
|
||||
- In the plugin UI, click "CONNECT TO MCP SERVER"
|
||||
- Verify it shows "Connected to MCP server"
|
||||
|
||||
## Step 6: Configure Claude Code MCP
|
||||
|
||||
Add the Penpot MCP server to Claude Code (if not already configured):
|
||||
|
||||
```bash
|
||||
claude mcp add penpot -t http http://localhost:9880/mcp
|
||||
```
|
||||
|
||||
**Important**: Use HTTP transport (`-t http`) with the `/mcp` endpoint. Do NOT use SSE transport - it causes "Server not initialized" errors.
|
||||
|
||||
## Step 7: Verify Connection
|
||||
|
||||
After connecting, restart the MCP connection in Claude Code:
|
||||
- Use `/mcp` command to reconnect the penpot server
|
||||
- The Penpot MCP tools (execute_code, export_shape, etc.) will then be available
|
||||
|
||||
Test with:
|
||||
```javascript
|
||||
mcp__penpot__execute_code(code="return penpot.currentPage.name;")
|
||||
```
|
||||
|
||||
## Important Notes
|
||||
|
||||
- Keep the Penpot plugin UI window open while using MCP tools
|
||||
- The MCP server must be running for the plugin to connect
|
||||
- If connection fails, check that the server is running on port 9880
|
||||
- Use HTTP transport (`-t http`), NOT SSE transport
|
||||
@@ -0,0 +1,203 @@
|
||||
---
|
||||
name: skill-create
|
||||
description: >
|
||||
Guidance for creating effective Claude Code skills (.skill packages).
|
||||
Use when the user wants to create, build, design, or iterate on a skill —
|
||||
including writing SKILL.md files, bundling scripts/references/assets,
|
||||
initializing new skills, packaging skills, or improving existing ones.
|
||||
Triggers on requests like "create a skill", "make a new skill",
|
||||
"build a skill for X", "package this skill", or "improve my skill".
|
||||
---
|
||||
|
||||
|
||||
# Skill Creator
|
||||
|
||||
## About Skills
|
||||
|
||||
Skills are modular, self-contained packages that extend Claude's capabilities
|
||||
by providing specialized knowledge, workflows, and tools. They transform Claude
|
||||
from a general-purpose agent into a specialized agent equipped with procedural
|
||||
knowledge that no model can fully possess.
|
||||
|
||||
### What Skills Provide
|
||||
|
||||
- **Specialized workflows** — Multi-step procedures for specific domains
|
||||
- **Tool integrations** — Instructions for working with specific file formats or APIs
|
||||
- **Domain expertise** — Company-specific knowledge, schemas, business logic
|
||||
- **Bundled resources** — Scripts, references, and assets for complex and repetitive tasks
|
||||
|
||||
## Core Principles
|
||||
|
||||
### Concise is Key
|
||||
|
||||
The context window is a public good. Skills share it with everything else Claude
|
||||
needs: system prompt, conversation history, other skills' metadata, and the
|
||||
actual user request.
|
||||
|
||||
Default assumption: Claude is already very smart. Only add context Claude doesn't
|
||||
already have. Challenge each piece of information: "Does Claude really need this
|
||||
explanation?" and "Does this paragraph justify its token cost?"
|
||||
|
||||
Prefer concise examples over verbose explanations.
|
||||
|
||||
### Set Appropriate Degrees of Freedom
|
||||
|
||||
Match specificity to the task's fragility and variability:
|
||||
|
||||
- **High freedom** (text-based instructions): Multiple approaches valid, decisions
|
||||
depend on context, heuristics guide the approach.
|
||||
- **Medium freedom** (pseudocode or scripts with parameters): Preferred pattern
|
||||
exists, some variation acceptable, configuration affects behavior.
|
||||
- **Low freedom** (specific scripts, few parameters): Operations are fragile and
|
||||
error-prone, consistency is critical, specific sequence must be followed.
|
||||
|
||||
Think of Claude as exploring a path: a narrow bridge with cliffs needs specific
|
||||
guardrails (low freedom), while an open field allows many routes (high freedom).
|
||||
|
||||
## Anatomy of a Skill
|
||||
|
||||
```
|
||||
skill-name/
|
||||
├── SKILL.md (required)
|
||||
│ ├── YAML frontmatter metadata (required)
|
||||
│ │ ├── name: (required)
|
||||
│ │ ├── description: (required)
|
||||
│ │ └── compatibility: (optional, rarely needed)
|
||||
│ └── Markdown instructions (required)
|
||||
└── Bundled Resources (optional)
|
||||
├── scripts/ - Executable code (Python/Bash/etc.)
|
||||
├── references/ - Documentation loaded into context as needed
|
||||
└── assets/ - Files used in output (templates, icons, fonts, etc.)
|
||||
```
|
||||
|
||||
### SKILL.md (required)
|
||||
|
||||
- **Frontmatter (YAML)**: `name` and `description` fields (required). Only these
|
||||
are read by Claude to determine when the skill triggers — be clear and
|
||||
comprehensive. The `compatibility` field is for environment requirements but
|
||||
most skills don't need it.
|
||||
- **Body (Markdown)**: Instructions and guidance. Only loaded AFTER the skill
|
||||
triggers.
|
||||
|
||||
### Bundled Resources (optional)
|
||||
|
||||
**Scripts (`scripts/`)** — Executable code for tasks requiring deterministic
|
||||
reliability or that are repeatedly rewritten.
|
||||
|
||||
**References (`references/`)** — Documentation loaded as needed into context.
|
||||
Keep SKILL.md lean; move detailed reference material, schemas, and examples here.
|
||||
If files are large (>10k words), include grep search patterns in SKILL.md.
|
||||
|
||||
**Assets (`assets/`)** — Files used in output, not loaded into context (templates,
|
||||
images, icons, boilerplate code, fonts).
|
||||
|
||||
### What to NOT Include
|
||||
|
||||
Do NOT create extraneous files like README.md, INSTALLATION_GUIDE.md,
|
||||
QUICK_REFERENCE.md, CHANGELOG.md, etc. The skill should only contain information
|
||||
needed for an AI agent to do the job.
|
||||
|
||||
## Progressive Disclosure
|
||||
|
||||
Skills use a three-level loading system:
|
||||
|
||||
1. **Metadata** (name + description) — Always in context (~100 words)
|
||||
2. **SKILL.md body** — When skill triggers (<5k words)
|
||||
3. **Bundled resources** — As needed (unlimited; scripts can run without reading)
|
||||
|
||||
Keep SKILL.md body under 500 lines. Split content into separate files when
|
||||
approaching this limit. Reference split files from SKILL.md with clear
|
||||
descriptions of when to read them.
|
||||
|
||||
### Disclosure Patterns
|
||||
|
||||
**Pattern 1: High-level guide with references** — Keep overview in SKILL.md,
|
||||
link to detail files loaded only when needed.
|
||||
|
||||
**Pattern 2: Domain-specific organization** — Organize content by domain
|
||||
(e.g., `references/finance.md`, `references/sales.md`) so only relevant content
|
||||
is loaded.
|
||||
|
||||
**Pattern 3: Conditional details** — Show basic content, link to advanced
|
||||
content loaded only when the user needs those features.
|
||||
|
||||
Guidelines:
|
||||
- Avoid deeply nested references — keep one level deep from SKILL.md
|
||||
- Structure longer reference files with a table of contents at the top
|
||||
|
||||
## Skill Creation Process
|
||||
|
||||
Follow these steps in order, skipping only with clear reason:
|
||||
|
||||
### Step 1: Understand the Skill with Concrete Examples
|
||||
|
||||
Skip only when usage patterns are already clearly understood.
|
||||
|
||||
Ask the user for concrete examples of how the skill will be used:
|
||||
- "What functionality should the skill support?"
|
||||
- "Can you give some examples of how this skill would be used?"
|
||||
- "What would a user say that should trigger this skill?"
|
||||
|
||||
Avoid overwhelming users — start with the most important questions.
|
||||
|
||||
### Step 2: Plan the Reusable Skill Contents
|
||||
|
||||
Analyze each example by considering how to execute from scratch and identifying
|
||||
what scripts, references, and assets would help with repeated execution.
|
||||
|
||||
Establish a list of reusable resources: scripts, references, and assets.
|
||||
|
||||
### Step 3: Initialize the Skill
|
||||
|
||||
Create the skill directory manually:
|
||||
|
||||
```
|
||||
mkdir -p <output-directory>/<skill-name>
|
||||
```
|
||||
|
||||
Then create `SKILL.md` with frontmatter and body. Add `scripts/`, `references/`,
|
||||
and `assets/` subdirectories only as needed.
|
||||
|
||||
Skip if iterating on an existing skill.
|
||||
|
||||
### Step 4: Edit the Skill
|
||||
|
||||
Remember the skill is for another Claude instance to use. Include beneficial,
|
||||
non-obvious information.
|
||||
|
||||
For design patterns, consult:
|
||||
- `references/workflows.md` — Sequential workflows and conditional logic
|
||||
- `references/output-patterns.md` — Template and example patterns
|
||||
|
||||
**Implementation order:**
|
||||
1. Start with reusable resources (`scripts/`, `references/`, `assets/`)
|
||||
2. Test added scripts by running them
|
||||
3. Delete unused example files from initialization
|
||||
4. Update SKILL.md
|
||||
|
||||
**Writing guidelines:** Always use imperative/infinitive form.
|
||||
|
||||
**Frontmatter:**
|
||||
- `name`: The skill name — use **domain-action** naming: `{domain}-{action}`.
|
||||
The domain is the system/area the skill operates on, the action is what it does.
|
||||
Examples: `pr-review`, `sprint-plan`, `docs-search`, `git-commit`, `debt-scan`.
|
||||
Multi-action wrappers (like `ticket`) can use the domain name alone.
|
||||
The directory name must match the `name` field.
|
||||
- `description`: Primary triggering mechanism. Include what the skill does AND
|
||||
specific triggers/contexts. All "when to use" info goes here (not in body).
|
||||
|
||||
**Body:** Instructions for using the skill and its bundled resources.
|
||||
|
||||
### Step 5: Validate the Skill
|
||||
|
||||
Check the skill manually:
|
||||
- Frontmatter has `name` and `description`
|
||||
- SKILL.md body is under 500 lines
|
||||
- No extraneous files (README.md, CHANGELOG.md, etc.)
|
||||
- Scripts are executable and tested
|
||||
- References are referenced from SKILL.md
|
||||
|
||||
### Step 6: Iterate
|
||||
|
||||
After real usage, notice struggles or inefficiencies, identify needed updates,
|
||||
implement changes, and test again.
|
||||
@@ -0,0 +1,87 @@
|
||||
---
|
||||
name: testmode
|
||||
description: >
|
||||
Run and interpret the ClideTestApp platform integration harness.
|
||||
Use after modifying toolchain resolution, IPC dispatch, extension
|
||||
lifecycle, theme loading, or native platform code. Also use when
|
||||
the user says "run testmode", "test the app", "smoke test", or
|
||||
"verify the build". Triggers on changes to: lib/kernel/src/toolchain.dart,
|
||||
lib/src/daemon/dispatcher.dart, lib/src/ipc/, lib/extension/,
|
||||
lib/kernel/src/theme/, lib/builtin/*/src/extension.dart,
|
||||
linux/CMakeLists.txt, macos/, Makefile (run targets), lib/main.dart.
|
||||
---
|
||||
|
||||
# Testmode harness
|
||||
|
||||
`ClideTestApp` is a lightweight Flutter app that boots the real platform
|
||||
layer, runs structured tests, prints results, and exits. It catches
|
||||
regressions that unit tests cannot see: missing binaries, broken
|
||||
subprocess wiring, IPC dispatch failures, extension activation order,
|
||||
and theme parse errors.
|
||||
|
||||
## Running
|
||||
|
||||
```bash
|
||||
make run-testmode # all categories, 60s timeout
|
||||
make run-testmode TESTMODE_CATEGORY=toolchain
|
||||
make run-testmode TESTMODE_CATEGORY=ipc
|
||||
make run-testmode TESTMODE_CATEGORY=extensions
|
||||
make run-testmode TESTMODE_TIMEOUT=120 # longer timeout for slow builds
|
||||
```
|
||||
|
||||
Results go to stdout **and** `/tmp/clide-testmode.log`. The last line
|
||||
of testmode output is machine-readable:
|
||||
|
||||
```
|
||||
[testmode:json] {"passed":33,"failed":0,"total":33,"failures":[]}
|
||||
```
|
||||
|
||||
Exit code is non-zero when any test fails. The Makefile verifies
|
||||
`"failed":0` in the log via `grep -q`.
|
||||
|
||||
## When to run which category
|
||||
|
||||
| Changed area | Category | Why |
|
||||
|---|---|---|
|
||||
| Toolchain, PATH, dugite, ptyc, shell | `toolchain` | Binary resolution + exec |
|
||||
| IPC envelope, dispatcher, schema | `ipc` | Round-trip + error contract |
|
||||
| Extension manifest, activate, contributions | `extensions` | Register + activate lifecycle |
|
||||
| Theme YAML, loader, palette | `extensions` | Theme parse is in this category |
|
||||
| Platform config (CMakeLists, pbxproj, Makefile) | `all` | Full rebuild validates everything |
|
||||
| Any doubt | `all` | ~30s, cheap insurance |
|
||||
|
||||
## Interpreting output
|
||||
|
||||
- `[testmode] exec | ... | OK` — subprocess ran, exit 0 or 1.
|
||||
- `[testmode] PASS | ...` — assertion passed (IPC / extension tests).
|
||||
- `[testmode] FAIL | ...` — assertion failed. The output column says why.
|
||||
- `[testmode] exec | ... | EXCEPTION` — binary not found or not executable.
|
||||
- `[testmode] exec | ... | TIMEOUT` — subprocess hung (5s limit).
|
||||
|
||||
If the app never prints `[testmode]` lines, the testmode gate in
|
||||
`main.dart` didn't fire — check that `CLIDE_TESTMODE` is set to a
|
||||
non-empty string (not a bare bool).
|
||||
|
||||
## Adding tests
|
||||
|
||||
All test logic lives in `lib/test_app.dart`. Pattern:
|
||||
|
||||
```dart
|
||||
// In the appropriate _run*Tests method:
|
||||
await _testExec('label', binary, ['args'], workDir);
|
||||
// or
|
||||
_addResult('label', boolCondition, 'detail string');
|
||||
```
|
||||
|
||||
New categories: add a `runFoo` bool from the `category` string,
|
||||
gate a new `_runFooTests` method, and add the category name to
|
||||
the Makefile help comment and this skill's table above.
|
||||
|
||||
Extension tests must include transitive dependencies — e.g.
|
||||
`GitExtension` requires `DiffExtension` to activate first.
|
||||
|
||||
## Source files
|
||||
|
||||
- `lib/test_app.dart` — all test logic and UI
|
||||
- `lib/main.dart:54` — testmode gate (`String.fromEnvironment`)
|
||||
- `Makefile:42–56` — `run-testmode` target with category variable
|
||||
@@ -0,0 +1,82 @@
|
||||
---
|
||||
name: ui-design
|
||||
description: >-
|
||||
Visual design guide for clide UI development — covers theme tokens,
|
||||
surface-specific token selection, control geometry/spacing/alignment,
|
||||
and Phosphor icons. Use when building or modifying widgets, panels,
|
||||
pane chrome, status indicators, tabs, list items, dialogs, or any
|
||||
visual surface. Triggers on: new widget code, theme-related changes,
|
||||
"which token", "what color", color/background questions, visual
|
||||
inconsistency fixes, "alignment off", "spacing", "padding", control
|
||||
geometry questions, new panel/pane/view development, adding or
|
||||
looking up Phosphor icons, icon codepoints.
|
||||
---
|
||||
|
||||
# UI design — clide visual surface guide
|
||||
|
||||
This skill bundles four concerns that all surface in widget work:
|
||||
|
||||
| Concern | Reference | When to read |
|
||||
|---------|-----------|--------------|
|
||||
| Token system, palette, typography | [`references/theme.md`](references/theme.md) | Designing or extending the theme pipeline; deciding whether to add a new token |
|
||||
| Token selection per surface | [`references/surface.md`](references/surface.md) | Building a new widget or modifying an existing one — "which token does this need" |
|
||||
| Spacing, alignment, control layout | [`references/geometry.md`](references/geometry.md) | Building tab strips, list items, buttons, anything where icons sit next to text or padded edges |
|
||||
| Phosphor icon usage and codepoints | [`references/icons.md`](references/icons.md) | Adding or referencing an icon |
|
||||
| Full Phosphor glyph table (1512, with codepoints) | [`references/phosphor-glyphs.md`](references/phosphor-glyphs.md) | Picking a specific glyph by name/look — find its codepoint, see if it's already defined |
|
||||
|
||||
Read the reference that matches the question. They cross-reference each
|
||||
other where relevant; you don't need to read all four.
|
||||
|
||||
## Universal rules
|
||||
|
||||
These apply across every reference and every surface:
|
||||
|
||||
- All colors come from `SurfaceTokens` via `ClideTheme.of(context).surface`.
|
||||
Never hardcode `Color(0xFF...)`.
|
||||
- Never use `Material*` or `Cupertino*` widgets or color constants — clide
|
||||
is `WidgetsApp` only (D-7).
|
||||
- Use `ClideText` for themed text; never bare `Text` in production widgets.
|
||||
- Typography: `clideFontMono` for code/paths/IDs, `clideFontCaption` for
|
||||
status/section headers, body inherits from `DefaultTextStyle`.
|
||||
|
||||
## Conversation-panel cards (T-305)
|
||||
|
||||
The Claude conversation stream has **three** card categories. They are NOT one
|
||||
shared wrapper widget — each is its own widget; they only share the spacing
|
||||
constants in `lib/widgets/src/clide_card_metrics.dart` (`kClideCardGap`,
|
||||
`kClideCardRadius`, `kClideCardHeaderPadH/V`, `kClideCardCounterSlotWidth`) so
|
||||
the stream reads as one rhythm. Change spacing there, not per-card.
|
||||
|
||||
1. **Dialog cards** — `ConversationCard` with a speaker **side stripe** (you /
|
||||
claude / agent). Prose/attribution; not collapsible.
|
||||
2. **Simple cards** — a single item shown fully open, never collapses (e.g. the
|
||||
image card). Standalone display; no chevron, no status chrome.
|
||||
3. **Collapsibles** — `ClideCollapserCard` (`lib/widgets/`). Every tool use is
|
||||
one, over a list of `1..N` inner item cards (a single tool = a 1-item list;
|
||||
there is no separate single-card path). Rules:
|
||||
- Whole card toggles; chevron hard against the **left** edge, the status tick
|
||||
(spinner/check/cross) hard against the **right** edge, the count in a
|
||||
fixed-width slot just inboard of it.
|
||||
- `color` drives the border + chevron/label tint (per-instance fidelity).
|
||||
- Collapsed ticker echoes the run's **last content line** as the title +
|
||||
count + aggregate status (computed by the caller, bubbled up from items).
|
||||
- Inner item cards are content + their **own** per-item status; they pass
|
||||
`margin: EdgeInsets.zero`-ish (a bottom margin matching the canvas) so the
|
||||
collapser pads the inner canvas **evenly on all sides** — never let an
|
||||
inner card jam under the header or against a frame edge.
|
||||
|
||||
Do NOT pull a dialog card's stripe or a simple card's config into the collapser,
|
||||
and do NOT nest collapsers — inside a run, tools render as the bare inner content
|
||||
card (`_ConversationTurn(collapseTools: false)`).
|
||||
|
||||
## Anti-patterns (cross-cutting)
|
||||
|
||||
- Borrowing another surface's token (`sidebarBackground` for hat bar) — give
|
||||
each surface its own token even if they share a palette key. See `theme.md`.
|
||||
- Hardcoded hex colors → use a token. See `surface.md` for which one.
|
||||
- `fontSize: 14` literal → use `clideFontCaption` or `clideFontMono`.
|
||||
- `fontFamily: 'JetBrainsMono'` literal → use `clideMonoFamily`.
|
||||
- Stacking edge padding on a padded parent + a padded child action → see
|
||||
`geometry.md` "no double edge padding".
|
||||
- Eyeballing pixel margins instead of working back from the constraint —
|
||||
the math matters; see `geometry.md` "uniform inner spacing".
|
||||
@@ -0,0 +1,201 @@
|
||||
# Geometry — spacing, alignment, control layout
|
||||
|
||||
Principles for placing icons, buttons, and text inside controls.
|
||||
Apply when building tab strips, list items, buttons with affordances,
|
||||
or anything where actions sit next to content.
|
||||
|
||||
> Constants live in `lib/widgets/src/spacing.dart` — pull from there
|
||||
> instead of inlining literals:
|
||||
>
|
||||
> | Concept | Constant |
|
||||
> |-------------------------|-------------------------|
|
||||
> | Hairline (2px) | `clideInsetHairline` |
|
||||
> | Tight inset (4px) | `clideInsetTight` |
|
||||
> | Uniform icon margin (6) | `clideInsetIcon` |
|
||||
> | Standard inset (8px) | `clideInsetStandard` |
|
||||
> | Text-content inset (12) | `clideInsetText` |
|
||||
> | Tight / standard gap | `clideGapTight` / `clideGapStandard` |
|
||||
> | Section / major gap | `clideGapSection` / `clideGapMajor` |
|
||||
> | Micro icon (10) | `clideIconMicro` |
|
||||
> | Standard icon (14) | `clideIconStandard` |
|
||||
> | Hit-target (16) | `clideIconHitTarget` |
|
||||
> | Control height (28) | `clideControlHeight` |
|
||||
|
||||
## Uniform inner spacing rule
|
||||
|
||||
Icons inside control surfaces should have **equal margin on every
|
||||
constrained side**. The "constrained sides" are top, bottom, and the
|
||||
side opposite to where content flows in.
|
||||
|
||||
The remaining side — where the text or other content sits — gets a
|
||||
larger, content-appropriate breathing room.
|
||||
|
||||
Example: tab close button (16×16 inside a 28-tall tab):
|
||||
|
||||
```
|
||||
top : 6 ┐
|
||||
bottom : 6 ├─ uniform: (28 − 16) / 2 = 6
|
||||
right : 6 ┘
|
||||
left : 8 ── content gap (separates from title text)
|
||||
```
|
||||
|
||||
The visual effect: the close button looks like a deliberate
|
||||
affordance with a calm, consistent border, not a glyph stuffed into
|
||||
the corner.
|
||||
|
||||
## No double-edge padding
|
||||
|
||||
When a fixed-size action (icon button, close ×) sits at the edge of
|
||||
a padded parent, the parent's padding on that edge should **not stack**
|
||||
with the action's own internal margin. Pick one place to hold the
|
||||
breathing room.
|
||||
|
||||
Wrong:
|
||||
|
||||
```dart
|
||||
Container(
|
||||
padding: EdgeInsets.symmetric(horizontal: 12), // tab pad: 12 right
|
||||
child: Row(children: [
|
||||
Expanded(child: title),
|
||||
SizedBox(width: 8), // gap: 8
|
||||
Container(width: 28, alignment: Center, // close: 6 internal margin
|
||||
child: Icon(close, size: 16)),
|
||||
]),
|
||||
)
|
||||
// Visible margin from icon right to outer right = 12 + 6 = 18px → too much
|
||||
```
|
||||
|
||||
Right:
|
||||
|
||||
```dart
|
||||
Container(
|
||||
padding: EdgeInsets.only(left: 12, right: 6), // pad matches icon margin
|
||||
child: Row(children: [
|
||||
Expanded(child: title),
|
||||
SizedBox(width: 8),
|
||||
Container(width: 16, height: 16, alignment: Center, // hit target = icon size
|
||||
child: Icon(close, size: 10)),
|
||||
]),
|
||||
)
|
||||
// Visible margin = 6 (parent right pad) ≈ 6 (top/bottom auto) → uniform
|
||||
```
|
||||
|
||||
## Two-column control pattern
|
||||
|
||||
For tab-shaped or row-shaped controls with a primary content area and
|
||||
a secondary action:
|
||||
|
||||
```dart
|
||||
Row(children: [
|
||||
Expanded(child: <content>), // takes remainder
|
||||
if (action != null) ...[
|
||||
SizedBox(width: 8), // standard gap
|
||||
<fixed-size action>, // shrinks to content
|
||||
],
|
||||
])
|
||||
```
|
||||
|
||||
- **Left column**: `Expanded`, holds the primary content (title,
|
||||
label, description). Aligned to the start of its space by default.
|
||||
- **Right column**: fixed natural width, holds the action (close,
|
||||
status, indicator). Sized to the icon, not to artificial padding.
|
||||
|
||||
The parent container's padding sits flush against both columns (see
|
||||
"no double-edge padding").
|
||||
|
||||
## Match perceived mass, not measured pixels
|
||||
|
||||
Glyphs vary in visual weight. A bold `+` looks heavier than a thin
|
||||
`×` at the same point size. When eyeballing alignment, trust the
|
||||
optical center over the geometric center.
|
||||
|
||||
In practice: if two icons measure to the same margin but one *looks*
|
||||
crowded, give the heavier glyph slightly more breathing room and
|
||||
trim the lighter one. For clide, this came up with the `×` close
|
||||
glyph vs the `+` add glyph — both at 14pt, but `+` reads as denser
|
||||
and is left in its 28-wide button without further padding, while
|
||||
`×` sits in a 16×16 hit area with 6px symmetric margin.
|
||||
|
||||
## Strip / row should fill the parent
|
||||
|
||||
Tab strips, status bars, and divider rows should span the full
|
||||
parent width, not size to their content. Without this, the strip
|
||||
looks like it floats inside the pane.
|
||||
|
||||
```dart
|
||||
Column(
|
||||
crossAxisAlignment: CrossAxisAlignment.stretch, // <-- this
|
||||
children: [
|
||||
_TabStrip(...),
|
||||
Expanded(child: _body(...)),
|
||||
],
|
||||
)
|
||||
```
|
||||
|
||||
Without `stretch`, Column gives loose width constraints and a
|
||||
`Container(height: tabHeight)` child sizes to its child's natural
|
||||
width — the strip ends mid-pane.
|
||||
|
||||
## Anchor strips with a divider
|
||||
|
||||
Add a 1px bottom border (`dividerColor`) to tab strips and any
|
||||
header strip that sits above content. Without it, the strip looks
|
||||
disconnected from the body and the perceived alignment slips.
|
||||
|
||||
```dart
|
||||
Container(
|
||||
height: 28,
|
||||
decoration: BoxDecoration(
|
||||
color: tokens.tabBarBackground,
|
||||
border: Border(bottom: BorderSide(color: tokens.dividerColor)),
|
||||
),
|
||||
child: ...,
|
||||
)
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Centering a glyph inside a "hover background" that's larger than
|
||||
the natural icon size, then surrounding the whole thing with a
|
||||
padded parent — the icon ends up far inside the visible edge.
|
||||
- Hardcoded `padding: EdgeInsets.symmetric(horizontal: 12)` on every
|
||||
control regardless of whether the right edge has an action — see
|
||||
"no double-edge padding".
|
||||
- Tab strip inside `Column` without `crossAxisAlignment.stretch` —
|
||||
the strip ends mid-pane.
|
||||
- `mainAxisSize.min` on the tab strip's outer Row when you actually
|
||||
want it to fill parent width — only use `min` for pill-shaped
|
||||
controls that should hug their content.
|
||||
- Eyeballing alignment without working back from a target margin in
|
||||
pixels. The math matters; see "uniform inner spacing".
|
||||
|
||||
## Ultrawide — clide is an IDE, assume wide screens (T-239)
|
||||
|
||||
The default `flutter_test` surface is 800px, but clide runs on 3440/5120
|
||||
ultrawide constantly. Two consequences:
|
||||
|
||||
- **Right-align with `Expanded`, not a `Spacer` that fights a flex sibling.**
|
||||
A `Row[ left…, Flexible(flex:1), Spacer(), right… ]` splits the free space
|
||||
between the loose flex item and the `Spacer` 50/50 — so the right group is
|
||||
pushed only *half* the free space. The drift is **proportional to width**:
|
||||
invisible at 800–1200px, ~1500px adrift at 3440px. To pin a right group to
|
||||
the edge regardless of width, put the left group in `Expanded(Row[...])`
|
||||
(it absorbs *all* free space; flex items flex within it) and let the right
|
||||
group trail at intrinsic width. See `StatusbarHost` in `lib/app.dart`.
|
||||
- **Test layout at ultrawide, not just the default surface.** Width-sensitive
|
||||
bugs hide at 800px. Drive the surface with `tester.view.physicalSize` —
|
||||
a wide `SizedBox` under the default 800px surface is CLAMPED to 800, so it
|
||||
doesn't actually test wide. Pattern in `test/app_statusbar_test.dart`
|
||||
(`pumpAt`): set `view.physicalSize`, assert key positions at a normal AND an
|
||||
ultrawide width. Audit tracked in T-241.
|
||||
|
||||
## Testing alignment
|
||||
|
||||
When iterating on a control's spacing:
|
||||
|
||||
1. State the target margin (e.g. "6px around the close icon, all
|
||||
sides except left").
|
||||
2. Map every contributing source: parent padding, gap SizedBoxes,
|
||||
container alignment offsets, icon-to-container size differences.
|
||||
3. Sum them. Adjust until they hit the target.
|
||||
4. Verify visually — perceived mass may justify a 1–2px tweak.
|
||||
@@ -0,0 +1,92 @@
|
||||
# Icons — Phosphor + clide-owned painters
|
||||
|
||||
## Phosphor Icons
|
||||
|
||||
The app bundles Phosphor Icons (v2.0.8, MIT) as TTF fonts at
|
||||
`assets/fonts/phosphor/` (regular, bold, fill weights).
|
||||
|
||||
**Glyph reference:** [`phosphor-glyphs.md`](phosphor-glyphs.md) — the full
|
||||
1512-glyph table (codepoint · kebab name · Pascal name). Grep it for the exact
|
||||
kebab name; **all 1512 are available** — no per-icon wiring (T-314).
|
||||
|
||||
### Using an icon
|
||||
|
||||
Reference a glyph by its **exact kebab-case name** — no codepoints in feature
|
||||
code; the label→codepoint table lives only in the generated
|
||||
`lib/widgets/src/icons/phosphor_glyphs.g.dart`:
|
||||
|
||||
```dart
|
||||
ClideIcon(PhosphorIcons.byName('arrow-clockwise'), size: 13)
|
||||
```
|
||||
|
||||
Or as a `TabContribution` icon field: `icon: PhosphorIcons.byName('lightbulb')`.
|
||||
|
||||
- An **unknown name** is a bug — it degrades to the `placeholder` error box so
|
||||
the mistake is visible. `phosphor_glyphs_test` asserts every `byName('…')`
|
||||
literal in `lib/` resolves, recovering the typo check a const would give.
|
||||
- For an **intentional blank** that still reserves the icon's box (alignment),
|
||||
use `const EmptyIconPainter()` — *not* a missing name.
|
||||
- Regenerate the map after a font bump: `dart run tool/gen_phosphor_glyphs.dart`.
|
||||
|
||||
**Bold weight:** pass `family: 'Phosphor-Bold'` to `PhosphorIconPainter`.
|
||||
**Fill weight:** `family: 'Phosphor-Fill'`.
|
||||
|
||||
**Bold weight:** pass `family: 'Phosphor-Bold'` to `PhosphorIconPainter`.
|
||||
**Fill weight:** `family: 'Phosphor-Fill'`.
|
||||
|
||||
## clide-owned painters
|
||||
|
||||
Some shapes are simple enough to paint directly without an icon
|
||||
font. Hand-rolled `ClideIconPainter` subclasses live under
|
||||
`lib/widgets/src/icons/`:
|
||||
|
||||
- `CheckIcon`, `ChevronIcon`, `CloseIcon` (`x.dart`)
|
||||
- `DotIcon`, `FolderIcon`, `GearIcon`
|
||||
- `GitBranchIcon`, `PlugIcon`, `SearchIcon`
|
||||
- `TerminalIcon`, `WarningIcon`
|
||||
|
||||
Use these for tiny, theme-aware glyphs (close ×, dropdown chevrons,
|
||||
status dots) where pulling in the Phosphor font weight would be
|
||||
overkill or where the visual needs to match the theme's stroke
|
||||
weight conventions.
|
||||
|
||||
Pattern for a new painter:
|
||||
|
||||
```dart
|
||||
class FoobarIcon extends ClideIconPainter {
|
||||
const FoobarIcon();
|
||||
|
||||
@override
|
||||
void paint(Canvas canvas, Color color) {
|
||||
final p = Paint()
|
||||
..color = color
|
||||
..strokeWidth = 0.10
|
||||
..strokeCap = StrokeCap.round;
|
||||
// Coordinates are 0..1 (the painter is given a unit square).
|
||||
canvas.drawLine(const Offset(0.2, 0.2), const Offset(0.8, 0.8), p);
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Sizing
|
||||
|
||||
Icon sizes used in clide (subject to consolidation under
|
||||
`ClideSpacing` — see T-86):
|
||||
|
||||
- `10` — micro: close × inside a tab
|
||||
- `13` — caption-row icons (sidebar, status bar)
|
||||
- `14` — standard inline icons (icon rail)
|
||||
- `16` — small icon hit-target outer container
|
||||
- `18`–`20` — emphatic / standalone icons
|
||||
|
||||
Pass `size:` to `ClideIcon`; the painter receives a unit-square
|
||||
canvas regardless. Color defaults to `globalForeground`; pass
|
||||
explicit `color:` for muted/active variants.
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Importing all of Phosphor — only declare codepoints we use.
|
||||
- Hand-painting a glyph that already exists in Phosphor at the right
|
||||
weight — use the font.
|
||||
- Hardcoded `Color` on icons — pass through the surface tokens
|
||||
(`globalForeground`, `globalTextMuted`, `panelActiveBorder`, etc.).
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,120 @@
|
||||
# Surface — token selection per surface type
|
||||
|
||||
Pick tokens based on **where** the widget lives, not what it does.
|
||||
|
||||
## Chrome (hat bar, status bar, sidebar, context panel, spines, drag handles)
|
||||
|
||||
```
|
||||
background → chromeBackground
|
||||
text → chromeForeground
|
||||
border → chromeBorder (1px)
|
||||
active text → globalForeground
|
||||
```
|
||||
|
||||
## Side panels (sidebar, context panel)
|
||||
|
||||
```
|
||||
background → chromeBackground (both sides — they're chrome frame)
|
||||
text → sidebarForeground
|
||||
hover → sidebarItemHover
|
||||
selected → sidebarItemSelected
|
||||
section head → sidebarSectionHeader (muted, used for "START", "FILES", etc.)
|
||||
```
|
||||
|
||||
Padding: 2px on outer edges, 0px on divider edge.
|
||||
|
||||
## Center column (workspace, Claude pane, editor)
|
||||
|
||||
```
|
||||
background → panelBackground
|
||||
text → globalForeground
|
||||
```
|
||||
|
||||
No padding — content fills edge to edge.
|
||||
|
||||
## Pane headers (`ClidePaneChrome`)
|
||||
|
||||
```
|
||||
background → panelHeader
|
||||
text (title) → panelHeaderForeground
|
||||
text (sub) → globalTextMuted
|
||||
```
|
||||
|
||||
## Tabs (`MultitabPane`, `ClideTabBar`)
|
||||
|
||||
```
|
||||
strip bg → tabBarBackground
|
||||
strip border → bottom: dividerColor (anchors strip to body)
|
||||
active fg → tabActiveForeground
|
||||
inactive fg → tabInactiveForeground
|
||||
active bg → panelHeader (elevated chrome)
|
||||
inactive bg → tabBarBackground (blends with strip)
|
||||
active border→ panelActiveBorder (top accent, 1.5px)
|
||||
side border → panelBorder
|
||||
```
|
||||
|
||||
For control geometry inside tabs (close button placement, padding,
|
||||
two-column title+action layout) see [`geometry.md`](geometry.md).
|
||||
|
||||
## List items (decisions, tickets, file rows, backlinks)
|
||||
|
||||
```
|
||||
background → (none / transparent)
|
||||
hover bg → listItemHoverBackground
|
||||
selected bg → listItemSelectedBackground
|
||||
text → listItemForeground / sidebarForeground (in sidebar)
|
||||
selected txt → listItemSelectedForeground
|
||||
```
|
||||
|
||||
In sidebar context, use `sidebarItemHover` not `listItemHoverBackground`.
|
||||
|
||||
## Buttons
|
||||
|
||||
```
|
||||
normal → buttonBackground / buttonForeground / buttonBorder
|
||||
hover → buttonHoverBackground
|
||||
active → buttonActiveBackground
|
||||
primary → buttonActiveBackground bg + globalBackground text
|
||||
subtle → listItemBackground / listItemHoverBackground (no border)
|
||||
```
|
||||
|
||||
## Dividers and separators
|
||||
|
||||
```
|
||||
line → dividerColor (always, everywhere)
|
||||
drag handle → 8px hit area, 1px visible line, panel bg fill
|
||||
hover line → panelActiveBorder
|
||||
```
|
||||
|
||||
## Status indicators
|
||||
|
||||
```
|
||||
success/ok → statusSuccess (green: done, added, connected)
|
||||
warning → statusWarning (amber: question, modified, missing)
|
||||
error → statusError (red: deleted, rejected, cancelled)
|
||||
info → statusInfo (blue: in_progress, modified)
|
||||
```
|
||||
|
||||
Map semantic states, not visual styles:
|
||||
|
||||
- `done` / `added` / `ok` → `statusSuccess`
|
||||
- `in_progress` / `modified` → `statusInfo`
|
||||
- `question` / `warning` → `statusWarning`
|
||||
- `cancelled` / `deleted` / `error` → `statusError`
|
||||
|
||||
## Overlays (dialogs, palette, tooltips)
|
||||
|
||||
```
|
||||
dialog bg → modalSurfaceBackground
|
||||
dialog border→ modalSurfaceBorder
|
||||
backdrop → modalOverlayBackground
|
||||
tooltip → tooltipBackground / tooltipForeground / tooltipBorder
|
||||
dropdown → dropdownBackground / dropdownForeground / dropdownBorder
|
||||
```
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- `globalBackground` for panel fill → use `panelBackground`
|
||||
- `listItemHoverBackground` in sidebar → use `sidebarItemHover`
|
||||
- Tab active bg = `panelBackground` → use `panelHeader` (elevated chrome)
|
||||
- Tab active border = `globalFocus` → use `panelActiveBorder`
|
||||
@@ -0,0 +1,100 @@
|
||||
# Theme — token system, palette, typography
|
||||
|
||||
## Token identity rule
|
||||
|
||||
Every visual surface gets its own named token. Never borrow a token from
|
||||
another surface just because they happen to resolve to the same color.
|
||||
|
||||
**Wrong:** `sidebarBackground` for the hat bar (the hat isn't a sidebar).
|
||||
**Right:** Create `chromeBackground` that resolves to the same palette key.
|
||||
|
||||
When two surfaces share a color:
|
||||
|
||||
1. **Same conceptual surface** (sidebar + context panel are both "side
|
||||
panels") → one shared token set is fine.
|
||||
2. **Different surfaces that happen to match** (hat bar + sidebar +
|
||||
status bar are all "chrome frame") → create a shared primitive in the
|
||||
palette/semantic layer (e.g. `bgChrome`) and give each surface its
|
||||
own token that maps to that primitive. This lets themes diverge them
|
||||
later without breaking widgets.
|
||||
|
||||
## Palette depth primitives
|
||||
|
||||
The palette layer has these depth primitives (defined in each theme YAML):
|
||||
|
||||
- `bg` (`#20202C`) — outermost root, behind everything
|
||||
- `bgSunken` (`#1A1A24`) — chrome frame: sidebar, hat, statusbar
|
||||
- `surface` (`#242838`) — elevated: pane headers, active tabs
|
||||
- `surfaceHi` (`#2C3046`) — interactive: hover states, selections
|
||||
|
||||
Chrome tokens (`chromeBackground` / `chromeForeground` / `chromeBorder`)
|
||||
are the shared root for all frame surfaces. They resolve to `bgSunken` /
|
||||
`textDim` / `border` in the palette. Themes can override them to diverge
|
||||
hat from sidebar from status bar if desired.
|
||||
|
||||
## Typography
|
||||
|
||||
Three constants — never hardcode sizes or families:
|
||||
|
||||
```
|
||||
family UI → inherited from DefaultTextStyle (JosefinSans Light 300)
|
||||
family mono → clideMonoFamily (JetBrainsMono)
|
||||
body size → clideFontBody (15)
|
||||
caption size → clideFontCaption (14) — status bar, section headers, git info
|
||||
mono size → clideFontMono (14) — terminal, code, paths, IDs
|
||||
```
|
||||
|
||||
Use `ClideText` for themed text. Set `muted: true` for secondary text
|
||||
(resolves to `globalTextMuted`). Set `fontFamily: clideMonoFamily` for
|
||||
code/paths/IDs. Don't set fontFamily for UI text — it inherits.
|
||||
|
||||
## Extension-owned domain colors
|
||||
|
||||
Extensions that need domain-specific color coding (ticket types, decision
|
||||
types, priority levels) should NOT add tokens to `SurfaceTokens`. Instead:
|
||||
|
||||
1. Create a color map class in the extension (e.g. `TicketTypeColors`).
|
||||
2. Ship dark and light presets, auto-selected via
|
||||
`ClideTheme.of(context).dark`.
|
||||
3. Store user overrides under `ext.<id>.colors` in settings.
|
||||
4. Reference: `lib/builtin/tickets/src/ticket_colors.dart`.
|
||||
|
||||
This keeps the core token surface lean and lets each extension own its
|
||||
palette. The pattern scales to any extension needing domain colors.
|
||||
|
||||
## Named themes are user contracts — never retune their palette (D-69)
|
||||
|
||||
A bundled theme that ships under a recognisable name (`clide`, `midnight`,
|
||||
`paper`, `terminal`, `catppuccin-*`, …) is a **user contract**. Its palette —
|
||||
including syntax tokens, status colours, and `borderHi` — **MUST NOT be
|
||||
retuned to pass a contrast gate**. Users pick Catppuccin because it looks like
|
||||
Catppuccin; quietly darkening a swatch to clear WCAG changes what they signed
|
||||
up for. Don't "fix" the artist's vision.
|
||||
|
||||
When a theme fails the contrast gate ([D-22](../../../governance/decisions/accessibility.md)),
|
||||
the fix is **a sibling `-hc` (high-contrast) theme**, not an edit to the
|
||||
faithful one (D-69):
|
||||
|
||||
- Keep `foo.yaml` pixel-faithful to the source palette.
|
||||
- Add `foo-hc.yaml` — same silhouette, but muted text / status chips / syntax
|
||||
tokens / focus border bumped to clear the strict (`extendedPairs`) gate. Only
|
||||
`-hc`/`-cb` variants are held to the extended set; base themes pass the
|
||||
baseline (`canonicalPairs`) only.
|
||||
- Register both in `main.dart` `_loadBundledThemes` **and** the bundled list in
|
||||
`test/a11y/contrast_test.dart`.
|
||||
|
||||
Mechanics: `canonicalPairs` (baseline, every theme) vs `extendedPairs`
|
||||
(strict, `-hc`/`-cb` only) live in `lib/kernel/src/theme/contrast.dart`. If a
|
||||
faithful palette is so soft it can't clear even the *baseline* chrome pairs,
|
||||
that's a design call — surface it, don't silently retune. (Catppuccin Latte
|
||||
hit exactly this on two chrome pairs.)
|
||||
|
||||
This is the same split VS Code ships: `Default Dark+` plus a separate
|
||||
`Default High Contrast`.
|
||||
|
||||
## Where to look in the codebase
|
||||
|
||||
- Full token list: `lib/kernel/src/theme/tokens.dart`
|
||||
- Resolver fallbacks: `lib/kernel/src/theme/resolver.dart`
|
||||
- Theme YAML example: `lib/kernel/src/theme/themes/clide.yaml`
|
||||
- Decision: D-43 (handoff), D-44 (four bundled themes), D-45 (syntax tokens)
|
||||
@@ -0,0 +1,58 @@
|
||||
#!/usr/bin/env python3
|
||||
"""Generate the Phosphor glyph reference page for the ui-design skill.
|
||||
|
||||
Mirrors `assets/fonts/phosphor/codepoints.csv` (the full bundled glyph set)
|
||||
into a readable, greppable markdown table at
|
||||
`.claude/skills/ui-design/references/phosphor-glyphs.md`, flagging which
|
||||
glyphs clide already defines in `lib/widgets/src/icons/phosphor.dart`.
|
||||
|
||||
Run from the repo root: python3 .claude/skills/ui-design/scripts/gen-phosphor-glyphs.py
|
||||
"""
|
||||
import csv
|
||||
import pathlib
|
||||
import re
|
||||
|
||||
ROOT = pathlib.Path(__file__).resolve().parents[4]
|
||||
CSV = ROOT / "assets/fonts/phosphor/codepoints.csv"
|
||||
DART = ROOT / "lib/widgets/src/icons/phosphor.dart"
|
||||
OUT = ROOT / ".claude/skills/ui-design/references/phosphor-glyphs.md"
|
||||
|
||||
# Codepoint -> camelCase accessor already defined in PhosphorIcons.
|
||||
defined = {}
|
||||
for m in re.finditer(r"static const (\w+) = PhosphorIconPainter\((0x[0-9a-fA-F]+)\)", DART.read_text()):
|
||||
defined[int(m.group(2), 16)] = m.group(1)
|
||||
|
||||
rows = list(csv.DictReader(CSV.open()))
|
||||
n_def = sum(1 for r in rows if int(r["codepoint"], 16) in defined)
|
||||
|
||||
lines = [
|
||||
"---",
|
||||
"name: phosphor-glyphs",
|
||||
"type: reference",
|
||||
"tags: [icons, phosphor, ui-design]",
|
||||
"---",
|
||||
"",
|
||||
"# Phosphor glyphs — full reference (v2.0.8)",
|
||||
"",
|
||||
f"All **{len(rows)}** glyphs bundled in clide's Phosphor font "
|
||||
"(`assets/fonts/phosphor/`, MIT). Generated from "
|
||||
"`assets/fonts/phosphor/codepoints.csv` — **do not hand-edit**; regenerate with "
|
||||
"`python3 .claude/skills/ui-design/scripts/gen-phosphor-glyphs.py`.",
|
||||
"",
|
||||
f"The **In clide** column flags the **{n_def}** glyphs already wired into "
|
||||
"`PhosphorIcons` (`lib/widgets/src/icons/phosphor.dart`) — reach for those first. "
|
||||
"To use any other glyph, add a one-line `static const` to that class with the "
|
||||
"codepoint below, then `ClideIcon(PhosphorIcons.<name>, size: 13)`. Keep additions "
|
||||
"to icons we actually use — don't bulk-import.",
|
||||
"",
|
||||
"| Codepoint | Name (kebab) | Pascal | In clide |",
|
||||
"|---|---|---|---|",
|
||||
]
|
||||
for r in rows:
|
||||
cp = r["codepoint"]
|
||||
accessor = defined.get(int(cp, 16))
|
||||
in_clide = f"`PhosphorIcons.{accessor}`" if accessor else ""
|
||||
lines.append(f"| `{cp}` | {r['name']} | {r['pascal_name']} | {in_clide} |")
|
||||
|
||||
OUT.write_text("\n".join(lines) + "\n")
|
||||
print(f"wrote {OUT.relative_to(ROOT)} — {len(rows)} glyphs, {n_def} already defined")
|
||||
@@ -0,0 +1,275 @@
|
||||
---
|
||||
name: whats-next
|
||||
description: >
|
||||
Surface the best batch of tickets to pick up next from pql. Walks the
|
||||
initiative/epic tree, filters to unblocked work, refines context via
|
||||
parallel agents (or `pql ticket refine` for empty descriptions), and
|
||||
optionally activates the batch on a fresh branch. Use when the user
|
||||
says "what's next", "next batch", "pick up work", or invokes
|
||||
/whats-next. NOT triggered by "what should we work on" in a design
|
||||
context — that's a discussion, not a batch selection.
|
||||
user-invocable: true
|
||||
allowed-tools: Bash, Read, Grep, Glob, Agent, AskUserQuestion
|
||||
---
|
||||
|
||||
# What's Next
|
||||
|
||||
Dependency-driven batch selection against pql. Three steps:
|
||||
batch selection → refinement review → batch activation.
|
||||
|
||||
Pql is the single source of truth for tickets and decisions in this repo
|
||||
(see [pql skill](../pql/SKILL.md) and [`decisions/README.md`](../../../decisions/README.md)). Always run from the repo root.
|
||||
|
||||
## Step 0: Sync state
|
||||
|
||||
Decisions on disk may be ahead of pql.db. Always sync before reading:
|
||||
|
||||
```bash
|
||||
pql decisions sync
|
||||
```
|
||||
|
||||
If `pql` is missing, stop and tell the user — don't fall back to grep.
|
||||
|
||||
---
|
||||
|
||||
## Step 1: Batch Selection
|
||||
|
||||
### 1a. Find active top-level work
|
||||
|
||||
Pql has no `milestone` concept; **initiatives** (and large **epics**) play
|
||||
that role. List in-flight top-level work:
|
||||
|
||||
```bash
|
||||
pql ticket list --status in_progress --pretty
|
||||
pql ticket list --status ready --pretty
|
||||
```
|
||||
|
||||
If nothing is `in_progress` or `ready` at the initiative/epic level,
|
||||
fall back to `pql plan status --pretty` for a dashboard read and ask the
|
||||
user which area to advance.
|
||||
|
||||
### 1b. Build the work landscape
|
||||
|
||||
For each candidate epic or initiative, read its full descendant subtree
|
||||
(nested, with the direct parent in `ancestors`) — one call, no client-side
|
||||
flattening (pql ≥ 1.6.0):
|
||||
|
||||
```bash
|
||||
pql ticket show <id> --tree --pretty # add --depth N to cap levels
|
||||
```
|
||||
|
||||
This replaces the old `--with-children` + hand-rolled status summaries.
|
||||
|
||||
### 1c. Filter to unblocked, actionable leaves
|
||||
|
||||
Don't walk blockers per ticket — pql does it. Compose the `list` filters
|
||||
(pql ≥ 1.6.0): `--leaf` (no children), `--unblocked` (every blocker is
|
||||
`done`/`cancelled`), and `--status` (since `--leaf`/`--unblocked` are
|
||||
structure/blocker-state only and would otherwise include `done` leaves):
|
||||
|
||||
```bash
|
||||
pql ticket list --under <epic-id> --leaf --unblocked --status backlog --pretty
|
||||
```
|
||||
|
||||
`--status` takes a SINGLE value (not a comma list — `backlog,ready` matches
|
||||
nothing and silently returns `[]`). The repo is almost all `backlog`; if a
|
||||
team uses `ready`, run it a second time with `--status ready`.
|
||||
|
||||
That returns exactly the actionable, unblocked leaves under the epic — the
|
||||
multi-result complement to `pql plan whatsnext` (which now also skips blocked
|
||||
tickets, for the single best pick). Note `--unblocked` only means no *ticket*
|
||||
blocker is open; a ticket "blocked on upstream" in prose (e.g. T-158) still
|
||||
shows — read the description before batching.
|
||||
|
||||
### 1d. Rank and group
|
||||
|
||||
Rank unblocked tickets by:
|
||||
|
||||
1. **Priority** (critical > high > medium > low) — read from ticket fields.
|
||||
2. **Epic proximity to done** — for each epic parent, compute
|
||||
`done_children / total_children`. Higher ratio ranks higher: finishing
|
||||
an epic unlocks downstream work and tightens the board.
|
||||
3. **Fan-out** — tickets that unblock the most other tickets rank higher.
|
||||
Approximate by scanning `pql ticket list --status backlog --pretty`
|
||||
and counting how many list this ticket in their blockers (use
|
||||
`--with-blockers` per candidate, or read `--jsonl` once and reduce in
|
||||
memory).
|
||||
|
||||
Group into **epic-sized batches**: tickets sharing a `parent_id`, or a
|
||||
logical cluster if no shared parent. If nothing groups naturally, batch
|
||||
by area (the directory the work touches, e.g. `lib/src/pty/`).
|
||||
|
||||
### 1e. Show the board
|
||||
|
||||
```bash
|
||||
pql ticket board --pretty
|
||||
```
|
||||
|
||||
This is the "what's currently in flight" view — the user wants to see
|
||||
WIP before committing to more.
|
||||
|
||||
### 1f. Present the recommended batch
|
||||
|
||||
Show the user:
|
||||
|
||||
- The recommended batch — IDs, titles, priorities, parent epic.
|
||||
- **Why this batch** — which epic it advances, what it unblocks downstream.
|
||||
- Current board state (WIP count vs. ready/backlog).
|
||||
- One or two alternative batches worth considering.
|
||||
|
||||
Wait for user confirmation before Step 2.
|
||||
|
||||
---
|
||||
|
||||
## Step 2: Ticket Refinement Review
|
||||
|
||||
Two cases — handle the cheap one first.
|
||||
|
||||
### 2a. Tickets with empty descriptions → use pql
|
||||
|
||||
If any ticket in the batch has no description, hand off to pql's
|
||||
built-in refinement flow:
|
||||
|
||||
```bash
|
||||
pql ticket refine list --pretty
|
||||
pql ticket refine next --pretty # full context for the next one
|
||||
pql ticket refine write T-NN '{"description":"..."}'
|
||||
```
|
||||
|
||||
Walk these with the user (AskUserQuestion per ticket if appropriate)
|
||||
before moving on.
|
||||
|
||||
### 2b. Tickets with descriptions → spawn refinement agents
|
||||
|
||||
For each ticket that already has a description but may still be
|
||||
under-specified, spawn one agent in parallel. Use `general-purpose`
|
||||
subagent_type (custom subagent_types lose SendMessage):
|
||||
|
||||
```
|
||||
Agent({
|
||||
subagent_type: "general-purpose",
|
||||
model: "sonnet",
|
||||
description: "Refine T-NN context",
|
||||
prompt: "You are the Refinement Manager for ticket T-NN.
|
||||
|
||||
Ticket: <title>
|
||||
Description: <body>
|
||||
Decision ref: <D-NN or Q-NN, if set>
|
||||
|
||||
Your job:
|
||||
1. Run `pql decisions show <decision_ref> --with-refs --pretty` and
|
||||
read the linked D/Q-record in decisions/<domain>.md.
|
||||
2. Grep decisions/questions-*.md for related Q-records.
|
||||
3. Verify referenced files, classes, and APIs actually exist in the
|
||||
current tree (Read/Grep). Flag dangling references.
|
||||
4. Cross-check against CLAUDE.md guardrails (single process, CLI-first,
|
||||
own the rendering stack, etc.) — flag tickets that conflict.
|
||||
|
||||
Assess: does an implementer have enough context to proceed without
|
||||
guessing? Report exactly one of:
|
||||
- READY: <one-paragraph summary of what the implementer needs to know>
|
||||
- GAPS: <list of specific ambiguities, each with 2–3 options>"
|
||||
})
|
||||
```
|
||||
|
||||
Run all agents in parallel (single message, multiple Agent tool calls).
|
||||
|
||||
### 2c. Resolve gaps
|
||||
|
||||
For each ticket that came back GAPS, surface ambiguities to the user
|
||||
via AskUserQuestion. After the user resolves, append the resolution to
|
||||
the ticket with `pql ticket append` (pql ≥ 1.6.0) — it appends
|
||||
blank-line-separated WITHOUT round-tripping the existing description, so
|
||||
there's no JSON-splicing or read-modify-write:
|
||||
|
||||
```bash
|
||||
pql ticket append T-NN "Refinement (<date>): <resolution>"
|
||||
# longer notes: pql ticket append T-NN --file note.md (or --stdin)
|
||||
```
|
||||
|
||||
(Use `append`, not `refine write` — the latter replaces the whole
|
||||
description from a JSON patch and forces you to re-send the existing body.)
|
||||
|
||||
If a gap really requires a new D-record (architectural choice, not just
|
||||
detail), flag it. Ask whether to write the D-record now (`pql decisions
|
||||
claim D <domain> "title"` then author the markdown) or defer with a note
|
||||
on the ticket.
|
||||
|
||||
### 2d. Present refined batch summary
|
||||
|
||||
Per ticket:
|
||||
|
||||
- READY summary, or the resolution the user just gave.
|
||||
- Linked D/Q-records.
|
||||
- Remaining blockers (should be none — re-check if Step 1 was a while ago).
|
||||
|
||||
Ask: "Batch ready. Activate?"
|
||||
|
||||
---
|
||||
|
||||
## Step 3: Batch Activation
|
||||
|
||||
### 3a. Mark tickets in_progress
|
||||
|
||||
Batch transition (comma-separated IDs):
|
||||
|
||||
```bash
|
||||
pql ticket status T-1,T-2,T-3 in_progress
|
||||
```
|
||||
|
||||
**Then persist it.** Ticket mutations (status here, and any `ticket new` in
|
||||
Step 2) land only in the gitignored `.pql/pql.db`. The post-checkout/post-merge
|
||||
hooks rebuild that DB from the committed changelog on every branch switch — so
|
||||
un-exported changes vanish silently the next time anyone switches branches. After
|
||||
creating or transitioning tickets, always:
|
||||
|
||||
```bash
|
||||
pql plan export # regenerates .pql/changelog/*.sql
|
||||
git add .pql/changelog && git commit # durable; survives rebuild
|
||||
```
|
||||
|
||||
See the [pql skill](../pql/SKILL.md#versioning-planning-state--data-loss-footgun-read-this).
|
||||
|
||||
### 3b. Branch? Default no.
|
||||
|
||||
Solo-dev flow on this repo — work lands directly on `main` (see recent
|
||||
`git log`). Don't create a topic branch unless the user explicitly asks.
|
||||
If they do, plain `git checkout -b` is fine; there is no `gh` CLI.
|
||||
|
||||
### 3c. Spawn implementation agents (optional)
|
||||
|
||||
If the user wants agents driving the work, spawn `general-purpose`
|
||||
subagents (`model: sonnet`) per ticket. Each prompt should include:
|
||||
|
||||
- Ticket details + the refinement summary from Step 2.
|
||||
- The full content of any linked D-record (Read it and inline it — don't
|
||||
just cite the ID; the agent has no project memory of it).
|
||||
- Repo guardrails the work touches (from CLAUDE.md — quote the relevant
|
||||
bullets, don't link).
|
||||
- A RULES block: write files only, no git commits, no destructive ops,
|
||||
message back when blocked or done.
|
||||
|
||||
### 3d. Report
|
||||
|
||||
End with a tight summary:
|
||||
|
||||
- Branch.
|
||||
- Tickets now `in_progress`.
|
||||
- Agents spawned (if any).
|
||||
- Next step: implement, then commit per the [git-commit skill](../git-commit/SKILL.md).
|
||||
|
||||
---
|
||||
|
||||
## Anti-patterns
|
||||
|
||||
- Don't skip Step 0 — stale `pql.db` makes the rest of the skill lie.
|
||||
- Don't leave ticket changes un-exported — `pql.db` is gitignored and the
|
||||
post-checkout/post-merge hooks rebuild it from the committed changelog, so a
|
||||
branch switch silently drops un-exported tickets. Always `pql plan export` +
|
||||
commit `.pql/changelog/` after mutating tickets (Step 3a).
|
||||
- Don't activate a batch the user hasn't confirmed.
|
||||
- Don't spawn refinement agents for tickets that have no description — use
|
||||
`pql ticket refine` instead; it's cheaper and writes back through the
|
||||
proper channel.
|
||||
- Don't reach for `gh` — this system doesn't have it. Plain `git` only.
|
||||
- Don't `cd` into subdirectories — run everything from the repo root.
|
||||
@@ -0,0 +1,23 @@
|
||||
root = true
|
||||
|
||||
[*]
|
||||
charset = utf-8
|
||||
end_of_line = lf
|
||||
insert_final_newline = true
|
||||
trim_trailing_whitespace = true
|
||||
indent_style = space
|
||||
indent_size = 2
|
||||
max_line_length = 160
|
||||
|
||||
[*.go]
|
||||
indent_style = tab
|
||||
indent_size = 4
|
||||
|
||||
[*.dart]
|
||||
indent_size = 2
|
||||
|
||||
[Makefile]
|
||||
indent_style = tab
|
||||
|
||||
[*.md]
|
||||
trim_trailing_whitespace = false
|
||||
@@ -0,0 +1 @@
|
||||
.pql/changelog/*.sql merge=union
|
||||
Executable
+9
@@ -0,0 +1,9 @@
|
||||
#!/bin/sh
|
||||
# pql: source .pql/hooks/post-checkout (rebuild pql.db on branch checkout).
|
||||
# The pql hook is untracked (a local `pql init` install), so a fresh
|
||||
# `git worktree add` has no .pql/hooks — source it only when present, and
|
||||
# always exit 0: post-checkout is best-effort and must never abort the
|
||||
# checkout / worktree creation.
|
||||
hook="$(git rev-parse --show-toplevel)/.pql/hooks/post-checkout"
|
||||
if [ -f "$hook" ]; then . "$hook"; fi
|
||||
exit 0
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# pql: source .pql/hooks/post-merge (planning-state auto-import on pull)
|
||||
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-merge"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# pql: source .pql/hooks/post-rewrite (rebuild pql.db after rebase / amend)
|
||||
. "$(git rev-parse --show-toplevel)/.pql/hooks/post-rewrite"
|
||||
Executable
+3
@@ -0,0 +1,3 @@
|
||||
#!/bin/sh
|
||||
# pql: source .pql/hooks/pre-commit (planning-state auto-export)
|
||||
. "$(git rev-parse --show-toplevel)/.pql/hooks/pre-commit"
|
||||
Executable
+57
@@ -0,0 +1,57 @@
|
||||
#!/usr/bin/env bash
|
||||
# Pre-push gate. Blocks the push if any quality check fails.
|
||||
#
|
||||
# Install: `make hooks` (points git core.hooksPath at .githooks/).
|
||||
# Bypass: never. If this runs slowly, fix the slow test; don't reach
|
||||
# for --no-verify (git-commit skill forbids it).
|
||||
#
|
||||
# Fast path (T-348): run the full ~2min test suite only when the push touches
|
||||
# lib/ (app + runtime Dart source) or pubspec.* (deps / version). test/,
|
||||
# assets/, docs, and tooling changes ride along with a lib change in practice,
|
||||
# and an otherwise-skipped push is covered by the next one that does touch lib.
|
||||
# The full suite is always available via `make push-check`, and the release CI
|
||||
# runs it forced on a tagged version. So a lib/pubspec-free push runs just the
|
||||
# instant decisions + changelog gates. A state we can't classify (unfetched
|
||||
# remote, new branch) runs the full gate.
|
||||
set -euo pipefail
|
||||
|
||||
cd "$(git rev-parse --show-toplevel)"
|
||||
|
||||
z40=0000000000000000000000000000000000000000
|
||||
|
||||
# Collect every file changed across the commits being pushed. git feeds the
|
||||
# hook one line per ref on stdin: <local-ref> <local-sha> <remote-ref> <remote-sha>.
|
||||
changed=""
|
||||
force_full=0
|
||||
while read -r _local_ref local_sha _remote_ref remote_sha; do
|
||||
[[ "$local_sha" == "$z40" ]] && continue # branch deletion — nothing to test
|
||||
if [[ "$remote_sha" == "$z40" ]]; then
|
||||
# New remote branch: diff from its merge-base with main, else play it safe.
|
||||
base="$(git merge-base "$local_sha" origin/main 2>/dev/null || true)"
|
||||
else
|
||||
base="$remote_sha"
|
||||
fi
|
||||
# If we can't resolve a base locally (e.g. the remote advanced and we haven't
|
||||
# fetched its objects), we can't classify the diff — run the full gate.
|
||||
if [[ -z "$base" ]] || ! git cat-file -e "$base^{commit}" 2>/dev/null; then
|
||||
force_full=1
|
||||
break
|
||||
fi
|
||||
changed+=$'\n'"$(git diff --name-only "$base" "$local_sha")"
|
||||
done
|
||||
|
||||
# Run the full gate when lib/ (app + runtime source) or pubspec.* (deps /
|
||||
# version) is touched, or when we couldn't classify above.
|
||||
needs_gate=1
|
||||
if [[ "$force_full" -eq 0 ]]; then
|
||||
trigger_files="$(printf '%s\n' "$changed" | grep -E '^(lib/|pubspec\.)' || true)"
|
||||
[[ -z "$trigger_files" ]] && needs_gate=0
|
||||
fi
|
||||
|
||||
if [[ "$needs_gate" -eq 0 ]]; then
|
||||
echo "==> pre-push: no lib/ or pubspec change — decisions + changelog gates, skipping tests"
|
||||
make decisions-validate changelog-gate
|
||||
else
|
||||
echo "==> pre-push: make push-check"
|
||||
make push-check
|
||||
fi
|
||||
@@ -0,0 +1,104 @@
|
||||
name: release
|
||||
|
||||
# Build + publish versioned Windows and Linux release bundles when the version
|
||||
# in pubspec.yaml changes on main. The `version` job only proceeds when the
|
||||
# v<version> tag doesn't already exist, so an unrelated pubspec edit is a no-op.
|
||||
#
|
||||
# FIRST CUT — neither build has run in CI yet (Windows has never been built at
|
||||
# all), so expect to iterate on these from the first run's logs. The repo's own
|
||||
# `make` targets are the build contract (gen-build-info + clide-cli + flutter
|
||||
# build, all wired in `make build`).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
paths: ['pubspec.yaml']
|
||||
workflow_dispatch:
|
||||
|
||||
permissions:
|
||||
contents: write # create the tag + the release
|
||||
|
||||
jobs:
|
||||
version:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.v.outputs.version }}
|
||||
fresh: ${{ steps.v.outputs.fresh }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with: { fetch-depth: 0 } # tags, to tell new vs. already-released
|
||||
- id: v
|
||||
shell: bash
|
||||
run: |
|
||||
version=$(awk -F': *' '/^version:/ {gsub(/[" ]/,"",$2); print $2; exit}' pubspec.yaml)
|
||||
echo "version=$version" >> "$GITHUB_OUTPUT"
|
||||
if git rev-parse "v$version" >/dev/null 2>&1; then
|
||||
echo "fresh=false" >> "$GITHUB_OUTPUT"
|
||||
echo "v$version already tagged — nothing to release."
|
||||
else
|
||||
echo "fresh=true" >> "$GITHUB_OUTPUT"
|
||||
echo "v$version is new — building."
|
||||
fi
|
||||
|
||||
build-linux:
|
||||
needs: version
|
||||
if: needs.version.outputs.fresh == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with: { channel: stable }
|
||||
- run: sudo apt-get update && sudo apt-get install -y ninja-build libgtk-3-dev
|
||||
- run: make dugite-fetch
|
||||
- run: make build # gen-build-info + clide-cli + flutter build linux
|
||||
- name: package
|
||||
run: tar -C build/linux/x64/release/bundle -czf clide-linux-x64-${{ needs.version.outputs.version }}.tar.gz .
|
||||
- uses: actions/upload-artifact@v4
|
||||
with: { name: linux, path: clide-linux-x64-*.tar.gz }
|
||||
|
||||
build-windows:
|
||||
needs: version
|
||||
if: needs.version.outputs.fresh == 'true'
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with: { channel: stable }
|
||||
- run: choco install -y make
|
||||
- name: build
|
||||
shell: bash
|
||||
run: make dugite-fetch && make build # MSVC + bash already on windows-latest
|
||||
- name: package
|
||||
shell: pwsh
|
||||
run: Compress-Archive -Path build/windows/x64/runner/Release/* -DestinationPath clide-windows-x64-${{ needs.version.outputs.version }}.zip
|
||||
- uses: actions/upload-artifact@v4
|
||||
with: { name: windows, path: clide-windows-x64-*.zip }
|
||||
|
||||
publish:
|
||||
needs: [version, build-linux, build-windows]
|
||||
if: needs.version.outputs.fresh == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: changelog notes for this version
|
||||
shell: bash
|
||||
run: |
|
||||
ver="${{ needs.version.outputs.version }}"
|
||||
# Pull the entries under `## [<version>]` — the changelog cut that the
|
||||
# version-bump commit lands per the changelog discipline — as the
|
||||
# release body; fall back to a one-liner if the section is absent.
|
||||
awk -v ver="$ver" '
|
||||
$0 ~ "^## \\[" ver "\\]" {grab=1; next}
|
||||
grab && /^## \[/ {exit}
|
||||
grab {print}
|
||||
' CHANGELOG.md > release-notes.md
|
||||
[ -s release-notes.md ] || echo "Release v$ver." > release-notes.md
|
||||
- uses: actions/download-artifact@v4
|
||||
with: { path: dist }
|
||||
- uses: softprops/action-gh-release@v2
|
||||
with:
|
||||
tag_name: v${{ needs.version.outputs.version }}
|
||||
name: clide v${{ needs.version.outputs.version }}
|
||||
body_path: release-notes.md # the version's CHANGELOG section
|
||||
generate_release_notes: true # + auto commit list appended
|
||||
files: dist/**/* # the built versioned bundles
|
||||
@@ -0,0 +1,144 @@
|
||||
name: test
|
||||
|
||||
# Linux CI for clide (GitHub Actions). Moved here from .gitea/workflows/ when CI
|
||||
# consolidated onto GitHub (the primary remote); the Gitea secondary has Actions
|
||||
# disabled. Pairs with windows.yml (ConPTY suite on windows-latest) and
|
||||
# release.yml (versioned release bundles).
|
||||
#
|
||||
# Steps go through the make targets (the repo's tooling-discipline rule: the
|
||||
# make layer sets up the environment — gen-build-info etc. — and stays correct
|
||||
# if a wrapped script moves).
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main]
|
||||
pull_request:
|
||||
|
||||
jobs:
|
||||
unit:
|
||||
name: unit + widget + golden + a11y + coverage gate
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with: { channel: stable, cache: true }
|
||||
- run: flutter pub get
|
||||
- name: install pql
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release download --repo postmeridiem/pql --pattern 'pql_*_Linux_x86_64.tar.gz' --output /tmp/pql.tgz
|
||||
tar -xzf /tmp/pql.tgz -C /tmp
|
||||
sudo install -m 0755 /tmp/pql /usr/local/bin/pql
|
||||
pql --version
|
||||
# The pql tests query the repo's vault, but .pql/pql.db is gitignored
|
||||
# (the post-checkout hook rebuilds it from the committed changelog).
|
||||
# A fresh CI checkout has the changelog but no db — materialize it,
|
||||
# and sync decision records from the governance/ DQR markdown tree
|
||||
# (tickets come from the changelog; decisions from `decisions sync`).
|
||||
pql plan import
|
||||
pql decisions sync
|
||||
# test-coverage runs the full fast suite WITH coverage (it includes the
|
||||
# a11y suite — see the push-check note in the Makefile), which is what
|
||||
# coverage-gate consumes.
|
||||
- run: make test-coverage
|
||||
- run: make coverage-gate
|
||||
|
||||
integration:
|
||||
name: integration_test (xvfb)
|
||||
runs-on: ubuntu-latest
|
||||
needs: unit
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with: { channel: stable, cache: true }
|
||||
- run: sudo apt-get update && sudo apt-get install -y xvfb ninja-build libgtk-3-dev
|
||||
- run: flutter pub get
|
||||
- name: install pql
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release download --repo postmeridiem/pql --pattern 'pql_*_Linux_x86_64.tar.gz' --output /tmp/pql.tgz
|
||||
tar -xzf /tmp/pql.tgz -C /tmp
|
||||
sudo install -m 0755 /tmp/pql /usr/local/bin/pql
|
||||
pql --version
|
||||
# The pql tests query the repo's vault, but .pql/pql.db is gitignored
|
||||
# (the post-checkout hook rebuilds it from the committed changelog).
|
||||
# A fresh CI checkout has the changelog but no db — materialize it,
|
||||
# and sync decision records from the governance/ DQR markdown tree
|
||||
# (tickets come from the changelog; decisions from `decisions sync`).
|
||||
pql plan import
|
||||
pql decisions sync
|
||||
- uses: coactions/setup-xvfb@v1
|
||||
with: { run: make test-integration }
|
||||
|
||||
startup-bundle:
|
||||
name: bundle smoke (xvfb 5s)
|
||||
runs-on: ubuntu-latest
|
||||
needs: unit
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with: { channel: stable, cache: true }
|
||||
- run: sudo apt-get update && sudo apt-get install -y xvfb ninja-build libgtk-3-dev
|
||||
- run: flutter pub get
|
||||
- name: install pql
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
gh release download --repo postmeridiem/pql --pattern 'pql_*_Linux_x86_64.tar.gz' --output /tmp/pql.tgz
|
||||
tar -xzf /tmp/pql.tgz -C /tmp
|
||||
sudo install -m 0755 /tmp/pql /usr/local/bin/pql
|
||||
pql --version
|
||||
# The pql tests query the repo's vault, but .pql/pql.db is gitignored
|
||||
# (the post-checkout hook rebuilds it from the committed changelog).
|
||||
# A fresh CI checkout has the changelog but no db — materialize it,
|
||||
# and sync decision records from the governance/ DQR markdown tree
|
||||
# (tickets come from the changelog; decisions from `decisions sync`).
|
||||
pql plan import
|
||||
pql decisions sync
|
||||
# Point the real release app's crash logs at an uploadable workspace dir
|
||||
# (T-436): if the bundle wedges on boot, the watchdog heartbeat/sample +
|
||||
# FileLogSink land here and get uploaded below. CLIDE_LOG=debug so the
|
||||
# file sink captures info/debug, not just the release-default warn.
|
||||
- name: bundle smoke (logs → artifact)
|
||||
env:
|
||||
CLIDE_LOG: debug
|
||||
CLIDE_LOG_DIR: ${{ github.workspace }}/clide-logs
|
||||
run: make smoke-bundle
|
||||
- name: Upload crash logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: bundle-crash-logs
|
||||
path: ${{ github.workspace }}/clide-logs
|
||||
if-no-files-found: ignore
|
||||
|
||||
# The web-WASM Playwright job is withheld: `flutter build web --wasm` cannot
|
||||
# compile the tree since the tree-sitter/PTY dart:ffi pivot (dart:ffi is
|
||||
# unavailable on the wasm target). Whether the web target gets conditional-
|
||||
# import fences or is dropped is an open question — see Q-50 in
|
||||
# governance/questions/architecture.md. Re-add the job (setup-node, npm
|
||||
# install + playwright install in tools/ui, `make test-e2e`) when Q-50
|
||||
# resolves toward keeping it.
|
||||
|
||||
docs:
|
||||
name: dart doc (lib API)
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with: { channel: stable, cache: true }
|
||||
- run: flutter pub get
|
||||
- name: dart doc --validate-links (fail on warning)
|
||||
run: |
|
||||
set -o pipefail
|
||||
dart doc --validate-links 2>&1 | tee dartdoc.log
|
||||
if grep -q "^ warning:" dartdoc.log; then
|
||||
echo "::error::dartdoc emitted warnings — see log above"
|
||||
exit 1
|
||||
fi
|
||||
- uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: dart-api-docs
|
||||
path: doc/api/
|
||||
@@ -0,0 +1,115 @@
|
||||
name: windows-soak
|
||||
|
||||
# ConPTY orphan-leak soak on a GitHub-hosted Windows runner — the cheap
|
||||
# alternative to a dedicated Windows VM. The freeze hypothesis (T-424) is that
|
||||
# each WindowsPty.start() leaks its conhost/OpenConsole host because the child
|
||||
# is not in a kill-on-close Job Object; across many runs those hosts pile up
|
||||
# until the box starves. tools/windows-verify/soak-conpty.ps1 reproduces that
|
||||
# WITHOUT crashing: it runs the ConPTY suite many times IN ONE job and counts
|
||||
# the hosts that survive each dart.exe exit. A throwaway runner is fine — we
|
||||
# watch the accumulation (the leading indicator), not the reboot. The repeated
|
||||
# runs happen inside this single job, so the leak can build up here even though
|
||||
# the runner is discarded afterwards (cf. the note in windows.yml, which only
|
||||
# runs the suite once).
|
||||
#
|
||||
# Diagnostic, never a gate: it always exits 0 and just publishes the verdict +
|
||||
# CSV. Runs on demand (workflow_dispatch) and when the soak kit itself changes.
|
||||
|
||||
on:
|
||||
workflow_dispatch:
|
||||
inputs:
|
||||
iterations:
|
||||
description: How many times to run the ConPTY suite (clean-path soak)
|
||||
default: "25"
|
||||
kill_iterations:
|
||||
description: Spawn+force-kill cycles (abrupt-death orphan probe)
|
||||
default: "15"
|
||||
ptys_per_iter:
|
||||
description: WindowsPty sessions spawned per kill cycle
|
||||
default: "2"
|
||||
push:
|
||||
branches: [windows-support]
|
||||
paths:
|
||||
- tools/windows-verify/**
|
||||
- .github/workflows/windows-soak.yml
|
||||
|
||||
jobs:
|
||||
conpty-soak:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
- run: flutter --version
|
||||
- run: flutter pub get
|
||||
- name: ConPTY orphan-leak soak
|
||||
shell: pwsh
|
||||
run: |
|
||||
$iters = "${{ github.event.inputs.iterations }}"
|
||||
if (-not $iters) { $iters = "25" }
|
||||
tools/windows-verify/soak-conpty.ps1 -Iterations ([int]$iters) -OutDir "$env:GITHUB_WORKSPACE/soak-out"
|
||||
- name: Publish verdict to job summary
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
$s = Get-ChildItem "$env:GITHUB_WORKSPACE/soak-out/*.summary.txt" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($s) { Get-Content $s.FullName | Add-Content $env:GITHUB_STEP_SUMMARY }
|
||||
- name: Upload soak CSV + summary
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: conpty-soak
|
||||
path: soak-out
|
||||
if-no-files-found: warn
|
||||
|
||||
conpty-kill-probe:
|
||||
# Abrupt-death half: force-kill the parent dart.exe mid-life (no close(),
|
||||
# no Job Object) and count the ConPTY hosts that survive. This is the path
|
||||
# the freeze hypothesis (T-424) actually implicates — the clean-path soak
|
||||
# above never exercises it. Diagnostic only; always succeeds.
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 30
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
- run: flutter --version
|
||||
- run: flutter pub get
|
||||
- name: ConPTY abrupt-death orphan probe
|
||||
shell: pwsh
|
||||
# CLIDE_LOG_DIR makes the probe emit FFI breadcrumbs (T-436): when a
|
||||
# parent is force-killed mid-life, its reader/waiter isolates' last
|
||||
# crumb ("ReadFile enter" / "WaitForSingleObject enter") is fsynced to
|
||||
# clide-pty.crumbs.log and uploaded below — naming what the wedged
|
||||
# isolate was doing at the instant of death.
|
||||
env:
|
||||
CLIDE_LOG_DIR: ${{ github.workspace }}/kill-crumbs
|
||||
run: |
|
||||
$iters = "${{ github.event.inputs.kill_iterations }}"
|
||||
if (-not $iters) { $iters = "15" }
|
||||
$ptys = "${{ github.event.inputs.ptys_per_iter }}"
|
||||
if (-not $ptys) { $ptys = "2" }
|
||||
tools/windows-verify/soak-conpty-kill.ps1 -Iterations ([int]$iters) -PtysPerIter ([int]$ptys) -OutDir "$env:GITHUB_WORKSPACE/kill-out"
|
||||
- name: Publish verdict to job summary
|
||||
if: always()
|
||||
shell: pwsh
|
||||
run: |
|
||||
$s = Get-ChildItem "$env:GITHUB_WORKSPACE/kill-out/*.summary.txt" -ErrorAction SilentlyContinue | Select-Object -First 1
|
||||
if ($s) { Get-Content $s.FullName | Add-Content $env:GITHUB_STEP_SUMMARY }
|
||||
- name: Upload kill-probe CSV + summary
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: conpty-kill-probe
|
||||
path: kill-out
|
||||
if-no-files-found: warn
|
||||
- name: Upload FFI breadcrumbs (last act of each killed reader/waiter)
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: conpty-kill-crumbs
|
||||
path: ${{ github.workspace }}/kill-crumbs
|
||||
if-no-files-found: ignore
|
||||
@@ -0,0 +1,48 @@
|
||||
name: windows
|
||||
|
||||
# Windows CI on GitHub-hosted runners — the only hosted Windows available, and
|
||||
# GitHub is clide's primary remote (the Gitea secondary is self-hosted Linux and
|
||||
# keeps running the Linux suite). This is the first real execution of the ConPTY
|
||||
# backend (lib/src/pty/windows_pty.dart), so expect genuine failures until the
|
||||
# Windows fixes land (T-424). Keep this OUT of required status checks until it's
|
||||
# reliably green — it reports + uploads artifacts without blocking merges. Each
|
||||
# run is a fresh, discarded runner, so the accumulation freeze (which needs
|
||||
# repeated runs on one machine) can't build up here.
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: [main, windows-support]
|
||||
pull_request:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
windows-pty:
|
||||
runs-on: windows-latest
|
||||
timeout-minutes: 25
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: subosito/flutter-action@v2
|
||||
with:
|
||||
channel: stable
|
||||
- run: flutter --version
|
||||
- run: flutter pub get
|
||||
# No `flutter analyze` here: it's platform-agnostic — the Linux job already
|
||||
# analyzes windows_pty.dart and everything else statically, and the
|
||||
# `flutter build windows` release job catches Windows-specific compile
|
||||
# errors. Skipping it also avoids needing `make gen-build-info`, since the
|
||||
# pty tests import the pty libraries directly, not the build_info-bearing
|
||||
# barrel (lib/clide.dart). This job's unique value is running real ConPTY.
|
||||
- name: ConPTY + Windows-arg unit tests
|
||||
# windows_pty_test.dart drives real ConPTY (it self-skips off-Windows);
|
||||
# the args/size suites are the pure-logic coverage. --timeout 60s so a
|
||||
# wedged reader fails fast instead of hanging the runner.
|
||||
run: dart test --concurrency=1 --timeout 60s test/pty/windows_pty_test.dart test/pty/windows_pty_args_test.dart test/pty/pty_size_test.dart
|
||||
- name: Upload test output / logs
|
||||
if: always()
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: windows-test-output
|
||||
# T-425 (crash-survivable FileLogSink) writes under %LOCALAPPDATA%\clide\logs;
|
||||
# add that path here once it lands so a freeze leaves a downloadable log.
|
||||
path: test/.test-output
|
||||
if-no-files-found: ignore
|
||||
+121
-168
@@ -1,188 +1,141 @@
|
||||
# ---> Python
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
*.py[cod]
|
||||
*$py.class
|
||||
# -- macOS sandbox (machine-specific paths; template is committed) ------
|
||||
/macos/Runner/DebugProfile.entitlements
|
||||
|
||||
# C extensions
|
||||
*.so
|
||||
# -- clean-house skill run history / skip ledger (machine-local) --------
|
||||
/governance/decisions/.clean-house-state.md
|
||||
|
||||
# Distribution / packaging
|
||||
.Python
|
||||
build/
|
||||
develop-eggs/
|
||||
dist/
|
||||
downloads/
|
||||
eggs/
|
||||
.eggs/
|
||||
lib/
|
||||
lib64/
|
||||
parts/
|
||||
sdist/
|
||||
var/
|
||||
wheels/
|
||||
share/python-wheels/
|
||||
*.egg-info/
|
||||
.installed.cfg
|
||||
*.egg
|
||||
MANIFEST
|
||||
# -- frame0 wireframe local↔Frame0 id mapping (machine-local) -----------
|
||||
**/*.idmap.json
|
||||
|
||||
# PyInstaller
|
||||
# Usually these files are written by a python script from a template
|
||||
# before PyInstaller builds the exe, so as to inject date/other infos into it.
|
||||
*.manifest
|
||||
*.spec
|
||||
!clide.spec
|
||||
# -- Flutter / Dart (single package at repo root) -----------------------
|
||||
/.dart_tool/
|
||||
/.packages
|
||||
/.flutter-plugins
|
||||
/.flutter-plugins-dependencies
|
||||
/build/
|
||||
/.idea/
|
||||
/.vscode/
|
||||
/*.iml
|
||||
/ios/Pods/
|
||||
/macos/Pods/
|
||||
/windows/flutter/ephemeral/
|
||||
/linux/flutter/ephemeral/
|
||||
/macos/Flutter/ephemeral/
|
||||
/bin/clide
|
||||
/bin/clide.exe
|
||||
# pubspec.lock is committed (supply-chain policy).
|
||||
|
||||
# Installer logs
|
||||
pip-log.txt
|
||||
pip-delete-this-directory.txt
|
||||
# UI harness (Playwright) — npm + output artefacts.
|
||||
tools/ui/node_modules/
|
||||
tools/ui/out/
|
||||
tools/ui/test-results/
|
||||
tools/ui/playwright-report/
|
||||
tools/ui/.serve.pid
|
||||
|
||||
# Unit test / coverage reports
|
||||
htmlcov/
|
||||
.tox/
|
||||
.nox/
|
||||
.coverage
|
||||
.coverage.*
|
||||
.cache
|
||||
nosetests.xml
|
||||
coverage.xml
|
||||
*.cover
|
||||
*.py,cover
|
||||
.hypothesis/
|
||||
.pytest_cache/
|
||||
cover/
|
||||
# -- dugite-native (bundled git, downloaded at build time) ---------------
|
||||
/native/dugite/
|
||||
|
||||
# Translations
|
||||
*.mo
|
||||
*.pot
|
||||
# -- dart doc output (generated by `dart doc`, uploaded as CI artefact) -
|
||||
/doc/
|
||||
|
||||
# Django stuff:
|
||||
*.log
|
||||
local_settings.py
|
||||
db.sqlite3
|
||||
db.sqlite3-journal
|
||||
# -- coverage output (regenerated by every `flutter test --coverage`).
|
||||
# Floor lives in pubspec.yaml `coverage_floor:`; nothing under
|
||||
# coverage/ is committed.
|
||||
/coverage/
|
||||
|
||||
# Flask stuff:
|
||||
instance/
|
||||
.webassets-cache
|
||||
# -- Tee'd flutter-test output (see Makefile `make t T=...`) ---------
|
||||
# Lives under test/ so the repo root stays uncluttered.
|
||||
/test/.test-output/
|
||||
|
||||
# Scrapy stuff:
|
||||
.scrapy
|
||||
# -- Build-time stamps generated by `make gen-build-info` -----------
|
||||
# version (from pubspec.yaml), commit (git short SHA), date (UTC).
|
||||
# Regenerated on every make build/run/test target.
|
||||
/lib/src/build_info.g.dart
|
||||
|
||||
# Sphinx documentation
|
||||
docs/_build/
|
||||
# -- Built C `clide` shell client (T-126). Source under ----------
|
||||
# native/clide-cli/ stays in git; the per-platform compiled binary
|
||||
# is built by `make clide-cli`.
|
||||
/native/linux-x64/clide
|
||||
/native/macos-arm64/clide
|
||||
/native/macos-x64/clide
|
||||
/native/windows-x64/clide.exe
|
||||
/native/windows-x64/clide.obj
|
||||
|
||||
# PyBuilder
|
||||
.pybuilder/
|
||||
target/
|
||||
# -- Test, coverage, profile output ------------------------------------
|
||||
*.test
|
||||
*.out
|
||||
*.prof
|
||||
*.pprof
|
||||
coverage.*
|
||||
/tmp/
|
||||
|
||||
# Jupyter Notebook
|
||||
.ipynb_checkpoints
|
||||
# Alchemist golden-test failure artefacts (diff images written when a
|
||||
# pixel-compare fails locally; the committed fixtures live under
|
||||
# app/test/goldens/goldens/{ci,linux}/).
|
||||
test/goldens/failures/
|
||||
|
||||
# IPython
|
||||
profile_default/
|
||||
ipython_config.py
|
||||
# -- pql per-repo state (index + planning DB, rebuilt from markdown) ----
|
||||
/.pql/*
|
||||
!/.pql/changelog/
|
||||
|
||||
# pyenv
|
||||
# For a library or package, you might want to ignore these files since the code is
|
||||
# intended to run in multiple environments; otherwise, check them in:
|
||||
# .python-version
|
||||
# -- SQLite index files (defensive; should never land at repo root) ----
|
||||
*.sqlite
|
||||
*.sqlite-journal
|
||||
*.sqlite-wal
|
||||
*.sqlite-shm
|
||||
*.db
|
||||
|
||||
# pipenv
|
||||
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
|
||||
# However, in case of collaboration, if having platform-specific dependencies or dependencies
|
||||
# having no cross-platform support, pipenv may install dependencies that don't work, or not
|
||||
# install all needed dependencies.
|
||||
#Pipfile.lock
|
||||
# -- Editor / IDE ------------------------------------------------------
|
||||
.vscode/
|
||||
.idea/
|
||||
*.swp
|
||||
*.swo
|
||||
|
||||
# UV
|
||||
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
#uv.lock
|
||||
# -- OS cruft ----------------------------------------------------------
|
||||
.DS_Store
|
||||
._*
|
||||
Thumbs.db
|
||||
|
||||
# poetry
|
||||
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
|
||||
# This is especially recommended for binary packages to ensure reproducibility, and is more
|
||||
# commonly ignored for libraries.
|
||||
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
|
||||
#poetry.lock
|
||||
|
||||
# pdm
|
||||
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
|
||||
#pdm.lock
|
||||
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
|
||||
# in version control.
|
||||
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
|
||||
.pdm.toml
|
||||
.pdm-python
|
||||
.pdm-build/
|
||||
|
||||
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
|
||||
__pypackages__/
|
||||
|
||||
# Celery stuff
|
||||
celerybeat-schedule
|
||||
celerybeat.pid
|
||||
|
||||
# SageMath parsed files
|
||||
*.sage.py
|
||||
|
||||
# Environments
|
||||
# -- Secrets -----------------------------------------------------------
|
||||
.env
|
||||
.venv
|
||||
env/
|
||||
venv/
|
||||
ENV/
|
||||
env.bak/
|
||||
venv.bak/
|
||||
|
||||
# Spyder project settings
|
||||
.spyderproject
|
||||
.spyproject
|
||||
|
||||
# Rope project settings
|
||||
.ropeproject
|
||||
|
||||
# mkdocs documentation
|
||||
/site
|
||||
|
||||
# mypy
|
||||
.mypy_cache/
|
||||
.dmypy.json
|
||||
dmypy.json
|
||||
|
||||
# Pyre type checker
|
||||
.pyre/
|
||||
|
||||
# pytype static type analyzer
|
||||
.pytype/
|
||||
|
||||
# Cython debug symbols
|
||||
cython_debug/
|
||||
|
||||
# PyCharm
|
||||
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
|
||||
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
|
||||
# and can be added to the global gitignore or merged into this file. For a more nuclear
|
||||
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
|
||||
#.idea/
|
||||
|
||||
# Ruff stuff:
|
||||
.ruff_cache/
|
||||
|
||||
# PyPI configuration file
|
||||
.pypirc
|
||||
|
||||
|
||||
# Clide specific
|
||||
.config/
|
||||
snapshot_report.html
|
||||
logs/
|
||||
|
||||
# Credentials
|
||||
.env.local
|
||||
.env.*.local
|
||||
*-credentials.md
|
||||
|
||||
# Claude Code
|
||||
.claude/
|
||||
# -- Claude Code — user-specific settings only; project-level settings
|
||||
# and skills are committed
|
||||
.claude/settings.local.json
|
||||
|
||||
# -- Legacy Python clide (under legacy/, pre-dating the Flutter rebuild).
|
||||
# Narrowly scoped so repo-root ignores stay focused on the current
|
||||
# toolchain.
|
||||
legacy/.venv/
|
||||
legacy/.ruff_cache/
|
||||
legacy/**/__pycache__/
|
||||
legacy/**/*.py[cod]
|
||||
legacy/**/*$py.class
|
||||
legacy/**/.pytest_cache/
|
||||
legacy/**/.mypy_cache/
|
||||
legacy/**/.ruff_cache/
|
||||
legacy/**/*.egg-info/
|
||||
legacy/**/htmlcov/
|
||||
legacy/**/.coverage
|
||||
legacy/**/.coverage.*
|
||||
.clide/settings.yaml
|
||||
.claude/skills/pql/
|
||||
|
||||
# frame0-wireframe local ID ↔ Frame0 ID mapping; per-machine state.
|
||||
*.idmap.json
|
||||
.pql/*
|
||||
!.pql/changelog/
|
||||
!.pql/pql-plan.json
|
||||
|
||||
# -- Local build / env artefacts (root-anchored; never tracked) ---------
|
||||
/.coverage
|
||||
/.venv/
|
||||
/dist/
|
||||
/logs/
|
||||
/firebase-debug.log
|
||||
# stray Python trees at the root (the real Dart suite is test/, singular)
|
||||
/clide/
|
||||
/tests/
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
# This file tracks properties of this Flutter project.
|
||||
# Used by Flutter tool to assess capabilities and perform upgrades etc.
|
||||
#
|
||||
# This file should be version controlled and should not be manually edited.
|
||||
|
||||
version:
|
||||
revision: "c9a6c484230f8b5e408ec57be1ef71dee1e77020"
|
||||
channel: "stable"
|
||||
|
||||
project_type: app
|
||||
|
||||
# Tracks metadata for the flutter migrate command
|
||||
migration:
|
||||
platforms:
|
||||
- platform: root
|
||||
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||
- platform: windows
|
||||
create_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||
base_revision: c9a6c484230f8b5e408ec57be1ef71dee1e77020
|
||||
|
||||
# User provided section
|
||||
|
||||
# List of Local paths (relative to this file) that should be
|
||||
# ignored by the migrate tool.
|
||||
#
|
||||
# Files that are not part of the templates will be ignored by default.
|
||||
unmanaged_files:
|
||||
- 'lib/main.dart'
|
||||
- 'ios/Runner.xcodeproj/project.pbxproj'
|
||||
@@ -0,0 +1,132 @@
|
||||
-- Auto-generated by migrate-ids (D-26). CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15.
|
||||
-- pql:created_by: migrate-ids
|
||||
-- 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);
|
||||
@@ -0,0 +1,37 @@
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AV8YXV4NYWS2ZQYW', '06FB0TNQM6KY9JVRKF22GGFMXR', '2026-05-18 11:59:28', '2026-05-18 11:59:28', NULL, 'ade2722e88d76b4ea6264545ff9b5398', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AV8YXV4NYWS2ZQYW', '06FB0TNQM78E2NAM4YK3A2AV6R', '2026-05-18 11:59:31', '2026-05-18 11:59:31', NULL, 'd996c0868b309fd7719ab4a8c2e0dd2b', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KY9JVRKF22GGFMXR', '06FB0TNQM78E2NAM4YK3A2AV6R', '2026-05-18 11:59:34', '2026-05-18 11:59:34', NULL, '05db5396eae627dbfae19415bf12802b', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AV8YXV4NYWS2ZQYW', '06FB0TNQM7J40S0A8Q8PC0PDY8', '2026-05-18 11:59:37', '2026-05-18 11:59:37', NULL, '44a606dc211dc172a0b7e45dca353555', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7J40S0A8Q8PC0PDY8', '06FB0TNQM6KEY6593FMW8Y40VC', '2026-05-18 11:59:41', '2026-05-18 11:59:41', NULL, 'a5958fcc4bc52bcba6c5d22119c1152a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AV8YXV4NYWS2ZQYW', '06FB0TNQM40XWGSN99DBDQDHJR', '2026-05-18 11:59:44', '2026-05-18 11:59:44', NULL, 'fd9d60cea050f1b2a8657664150e977c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78E2NAM4YK3A2AV6R', '06FB0TNQM40XWGSN99DBDQDHJR', '2026-05-18 11:59:47', '2026-05-18 11:59:47', NULL, '32e0aec7ec29068519dbd31c8a8cdc80', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AV8YXV4NYWS2ZQYW', '06FB0TNQM5MW2V3QCMWKMY4VQM', '2026-05-18 11:59:51', '2026-05-18 11:59:51', NULL, 'cd33e0846477b82b5cf7e00781783b8c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78E2NAM4YK3A2AV6R', '06FB0TNQM5ZSRZARG7SXCC71NM', '2026-05-18 11:59:54', '2026-05-18 11:59:54', NULL, '4ce716dceb22197381bbf6f7f5cb339c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KEY6593FMW8Y40VC', '06FB0TNQM5ZSRZARG7SXCC71NM', '2026-05-18 11:59:54', '2026-05-18 11:59:54', NULL, '51d502ceb0a41091beb46513618b5b04', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KY9JVRKF22GGFMXR', '06FB0TNQM5ZSRZARG7SXCC71NM', '2026-05-18 11:59:54', '2026-05-18 11:59:54', NULL, '7f4410ea5c072aaac3317a9ed4a99912', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7J40S0A8Q8PC0PDY8', '06FB0TNQM5ZSRZARG7SXCC71NM', '2026-05-18 11:59:54', '2026-05-18 11:59:54', NULL, 'ccd6da9f9358922bc665133b792c65ed', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AV8YXV4NYWS2ZQYW', '06FB0TNQM5ZSRZARG7SXCC71NM', '2026-05-18 11:59:54', '2026-05-18 11:59:54', NULL, 'd89ba28926b6472133096d779a20f079', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6SRMD12DMXCX6N7DM', '06FB0TNQM6E3X5WWRS09GCDGK8', '2026-05-22 15:59:56', '2026-05-22 15:59:56', NULL, '129c875883b4e5a53cdc0683045ace03', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54N0ZDJRS7G6MP1XC', '06FB0TNQM6E3X5WWRS09GCDGK8', '2026-05-22 15:59:56', '2026-05-22 15:59:56', NULL, '1b4d4dcf6933819d32df98dff4f89d83', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM653J6604KYDK6RT8R', '06FB0TNQM5AVSMKD15KBJYX48W', '2026-05-22 15:59:56', '2026-05-22 15:59:56', NULL, '1c81cdedd045c46a2f1264bcd1067d11', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6E3X5WWRS09GCDGK8', '06FB0TNQM4V1PZ0EK6G60699EM', '2026-05-22 15:59:56', '2026-05-22 15:59:56', NULL, '23b6fb946d9de5e730bd186a9ed350e0', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6E3X5WWRS09GCDGK8', '06FB0TNQM5AVSMKD15KBJYX48W', '2026-05-22 15:59:56', '2026-05-22 15:59:56', NULL, '876ead25db98c91cadc5ea7caa65b513', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4EQAERS4J9X9RAETR', '06FB0TNQM4V1PZ0EK6G60699EM', '2026-05-22 15:59:56', '2026-05-22 15:59:56', NULL, '91e9aa2d0ffd1f32a52bb5cdf019f3fc', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM653J6604KYDK6RT8R', '06FB0TNQM5V9RNW2BGT5GHRTRG', '2026-05-22 15:59:56', '2026-05-22 15:59:56', NULL, 'a8c93af5185bec5604246916460fea08', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4EQAERS4J9X9RAETR', '06FB0TNQM653J6604KYDK6RT8R', '2026-05-22 15:59:56', '2026-05-22 15:59:56', NULL, 'f05aa1ae12d538345bc451d332395b9c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5KN50C7A723SEKZXG', '06FB0TNQM5V9RNW2BGT5GHRTRG', '2026-05-23 06:08:24', '2026-05-23 06:08:24', NULL, 'a0eacf85ca484d159a3dcc6c170f1760', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM77GWREHKA9K45P020', '06FB0TNQM6YSQYCE096R2SFRXG', '2026-05-23 09:52:40', '2026-05-23 09:52:40', NULL, 'bc99438a38a478190cae7e43c4342526', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM77GWREHKA9K45P020', '06FB0TNQM7BJS5E1JJGSJ9XHG0', '2026-05-23 09:52:43', '2026-05-23 09:52:43', NULL, '2a604702bb0023631d5a98810f695439', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM77GWREHKA9K45P020', '06FB0TNQM4XM8EY708NAM1JK00', '2026-05-23 09:52:45', '2026-05-23 09:52:45', NULL, '4303c0c52bfcc9f3dbd48f784ef1adf8', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AHSN3BXZFFWS0EPM', '06FB0TNQM78W48QX3JZP0QYAVW', '2026-05-24 16:27:29', '2026-05-24 16:27:29', NULL, '01db76f1732be386944e3da80fcdc0b4', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AHSN3BXZFFWS0EPM', '06FB0TNQM7G7MK8MP3BGQC36HG', '2026-05-24 16:27:29', '2026-05-24 16:27:29', NULL, 'c71b201e5cbe1f75ccda37657808e448', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AHSN3BXZFFWS0EPM', '06FB0TNQM7KA37YDBJ89GJ27H4', '2026-05-24 16:27:29', '2026-05-24 16:27:29', NULL, 'c9369a0c50aa82e2afa9a7fa70534b61', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AHSN3BXZFFWS0EPM', '06FB0TNQM48P8T23ZPR0BRK6PR', '2026-05-24 16:27:37', '2026-05-24 16:27:37', NULL, '7f6b0dddd7ba67b4db049f3d69b7c807', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48P8T23ZPR0BRK6PR', '06FB0TNQM654XV2TG4K0RVV1CW', '2026-05-24 16:27:43', '2026-05-24 16:27:43', NULL, 'ad5cfd3ff355cd44017ea0898ea14780', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48P8T23ZPR0BRK6PR', '06FB0TNQM6FMAHXM1XAHHCQC60', '2026-05-24 16:27:46', '2026-05-24 16:27:46', NULL, '7365404994faf148b33ab5ddc83fa01d', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM654XV2TG4K0RVV1CW', '06FB0TNQM6FMAHXM1XAHHCQC60', '2026-05-24 16:27:52', '2026-05-24 16:27:52', NULL, 'e3930f2673b82e4e6e0938450c522946', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48P8T23ZPR0BRK6PR', '06FB0TNQM71N3YMS82BAYD3M9R', '2026-05-24 16:27:56', '2026-05-24 16:27:56', NULL, '856fdd59d24643888245f06c621f40b8', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM654XV2TG4K0RVV1CW', '06FB0TNQM6V6TKRPA106D5SMMM', '2026-05-25 09:16:24', '2026-05-25 09:16:24', NULL, '329210f950241271814a8ed1255aa993', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48P8T23ZPR0BRK6PR', '06FB0TNQM67ET3VQP90WRXVSMG', '2026-05-25 09:16:24', '2026-05-25 09:16:24', NULL, '8a8027ce60a1ec2566c0b32f67f7759b', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4W624VD4JW1GHDHAR', '06FB0TNQM7WE0SD9N4HCP9BB64', '2026-05-25 09:26:48', '2026-05-25 09:26:48', NULL, '388878e0c968776f9bc7a61934dbd3d6', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6YYG4X5R1GX97TJB4', '06FB0TNQM452XPSG99PA60ZZ48', '2026-05-31 18:01:19', '2026-05-31 18:01:19', NULL, '06723eeaf4ac4b3ca32e24df249de904', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
@@ -0,0 +1,37 @@
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM65R8CNFNE1MA5GGR0', '06FB0TNQM4PH1J6ENYT4PJZRHG', '2026-06-01 18:48:57', '2026-06-01 18:48:57', NULL, '1faf4292ceab6cfe3550dfedad554efe', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4PH1J6ENYT4PJZRHG', '06FB0TNQM5REFZXMY8ESWN0Z90', '2026-06-01 18:48:57', '2026-06-01 18:48:57', NULL, 'ca9731f8247e19ee44e99f51b4c33ba2', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M83TTXHRXESFZY14', '06FB0TNQM5REFZXMY8ESWN0Z90', '2026-06-01 18:48:58', '2026-06-01 18:48:58', NULL, '097bef3723659e681584982688cb242f', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54WTNQDMYT0AVR4WM', '06FB0TNQM78J2H2S0R79SSSPW8', '2026-06-02 18:13:52', '2026-06-02 18:13:52', NULL, '112f7b091860de0dc7246f2c1b0eea45', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6Y5SET5WGSS2W78F8', '06FB0TNQM758PXMJH16PA3JHFW', '2026-06-02 18:13:52', '2026-06-02 18:13:52', NULL, '497eb0cb7db0b11194031874d6e8d89d', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7171NRB1T82TGP5RG', '06FB0TNQM78J2H2S0R79SSSPW8', '2026-06-02 18:13:52', '2026-06-02 18:13:52', NULL, '5102b2f239c5c18631997cbc1f2d998c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6EGSD0SB7NYNSP1BM', '06FB0TNQM758PXMJH16PA3JHFW', '2026-06-02 18:13:52', '2026-06-02 18:13:52', NULL, '9d0c7bbff8117a3569cf3b7937228184', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4C2PSN2GVJY9JDSZG', '06FB0TNQM7171NRB1T82TGP5RG', '2026-06-02 18:13:52', '2026-06-02 18:13:52', NULL, 'd935e3d2978a3584efb0767e75737e84', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45HHRBCRMV2166Y6M', '06FB0TNQM5YYMMKFD5Z8WG9F60', '2026-06-07 08:49:16', '2026-06-07 08:49:16', NULL, '11d94aab7a635da1ffb610aa2ade2061', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM551VZ2NYV6PD0XQRC', '06FB0TNQM4XBRZJ6DEHFWREWC0', '2026-06-07 10:42:46', '2026-06-07 10:42:46', NULL, 'daa1e2dd097721230deca67500c1c00d', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM551VZ2NYV6PD0XQRC', '06FB0TNQM67PDXAFTPPW7B1WDR', '2026-06-07 10:42:50', '2026-06-07 10:42:50', NULL, '50bb0fcfafb5aacdd747bd9fcd6a3ea6', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB234WP4Y6Q16A0HFW8BSXMG', '2026-06-10 11:12:11', '2026-06-10 11:12:11', NULL, 'e0876fe73623c4063849a13f99d552f5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB234WP4Y6Q16A0HFW8BSXMG', '2026-06-10 11:12:11', '2026-06-10 11:12:14', NULL, 'cd39602b20a0681e3cca2dfee38d1734', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2ESCR4V6V07CRH18FBCDN8', '2026-06-10 11:12:18', '2026-06-10 11:12:18', NULL, 'c74a2c5d562157c24c11c8fa5d04799e', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2ETJQP0CT6X7W3CWZ6NS9G', '2026-06-10 11:12:20', '2026-06-10 11:12:20', NULL, 'cb20764a77d2a15290ed61a186542095', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2EV29HSK6EJ5VF50R87VC4', '2026-06-10 11:12:25', '2026-06-10 11:12:25', NULL, 'dd9434426790edcaa556221f74c431ba', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', '06FB2G1WD1839Z90AQ5C0BHNV4', '2026-06-10 11:17:25', '2026-06-10 11:17:25', NULL, '5535e3d16bee2a2cdfcbeb84ea3fca99', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', '06FB2TY91VHK7TPKPMZ11EG3TM', '2026-06-10 12:04:53', '2026-06-10 12:04:53', NULL, 'ed3717d8f6467c0a77236eda670efce5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', '06FB2TY91VHK7TPKPMZ11EG3TM', '2026-06-10 12:04:53', '2026-06-10 12:05:01', '2026-06-10 12:05:01', '137cd047c9eaec01cac955b49fedd839', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DJZDDZ00BSA04B660RS7M', '06FB3DKQQJ583944DG8561VQ3G', '2026-06-10 13:27:04', '2026-06-10 13:27:04', NULL, '73383d1011fbad641395f27271b141e6', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DKQQJ583944DG8561VQ3G', '06FB3DMF20SYFDT6WX2RFBQXKW', '2026-06-10 13:27:04', '2026-06-10 13:27:04', NULL, '45533aa2b124e6547a3804294fcf3aaf', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DJZDDZ00BSA04B660RS7M', '06FB3DQEMTDHF8SV27AKAB8JHW', '2026-06-10 13:27:05', '2026-06-10 13:27:05', NULL, '0eb2faf91e36a4d37a8a3aa97a8edb71', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DKQQJ583944DG8561VQ3G', '06FB3DQEMTDHF8SV27AKAB8JHW', '2026-06-10 13:27:05', '2026-06-10 13:27:05', NULL, 'b329ff8925097a977f528e7e114d6055', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DMF20SYFDT6WX2RFBQXKW', '06FB3DN94MBCTYJW17ZCYVSXE0', '2026-06-10 13:27:08', '2026-06-10 13:27:08', NULL, 'f494f6efad2377e6fc6d0bbf7ae35f07', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DQEMTDHF8SV27AKAB8JHW', '06FB3DN94MBCTYJW17ZCYVSXE0', '2026-06-10 13:27:09', '2026-06-10 13:27:09', NULL, 'e5d02fc3a99106d29a9ce6af0626f68e', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DMF20SYFDT6WX2RFBQXKW', '06FB3DNQZKV20F7YG5PJH8V8SM', '2026-06-10 13:27:09', '2026-06-10 13:27:09', NULL, '1d5185ae9c9676bd70d0e02e3a5e79a1', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DQEMTDHF8SV27AKAB8JHW', '06FB3DNQZKV20F7YG5PJH8V8SM', '2026-06-10 13:27:10', '2026-06-10 13:27:10', NULL, '90dca94aa700c143290b2b1afaca09ed', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DMF20SYFDT6WX2RFBQXKW', '06FB3DP48FS33CQGRDF7EB9GT0', '2026-06-10 13:27:10', '2026-06-10 13:27:10', NULL, '9b9081edc77f0e9a4b689bbc191771db', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DJZDDZ00BSA04B660RS7M', '06FB3DQEMTDHF8SV27AKAB8JHW', '2026-06-10 13:27:05', '2026-06-12 01:11:12', NULL, '17f1c884268a172f803f407a2ad47c8c', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DQEMTDHF8SV27AKAB8JHW', '06FB3DN94MBCTYJW17ZCYVSXE0', '2026-06-10 13:27:09', '2026-06-12 01:11:17', NULL, 'a67368ff15089dce57838840632fe07e', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DQEMTDHF8SV27AKAB8JHW', '06FB3DNQZKV20F7YG5PJH8V8SM', '2026-06-10 13:27:10', '2026-06-12 01:11:22', NULL, '0ec8ff6c455136e45fbb1d06a2a690f1', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DMF20SYFDT6WX2RFBQXKW', '06FB3DP48FS33CQGRDF7EB9GT0', '2026-06-10 13:27:10', '2026-06-12 01:11:26', NULL, '3c985828c591c88d492eb261c6d26d33', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DQEMTDHF8SV27AKAB8JHW', '06FB3DMF20SYFDT6WX2RFBQXKW', '2026-06-12 01:11:31', '2026-06-12 01:11:31', NULL, '81fd318a43138a3bef82e31f928ff0b2', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKMXSVCE98K1H76N00TYCQR', '2026-06-12 03:16:35', '2026-06-12 03:16:35', NULL, '28d9785a1f9696b6b579a1dbb9fdb889', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN09R21H3AWR2Q2ZTSGNSW', '2026-06-12 03:16:40', '2026-06-12 03:16:40', NULL, '7e00348c10432c65b03f9dce36520e48', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMXSVCE98K1H76N00TYCQR', '06FBKN2MP35NPPK1BRDYY2M428', '2026-06-12 03:16:44', '2026-06-12 03:16:44', NULL, 'a6b1c20279ac30f692a30c802cf8f3e5', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
INSERT INTO ticket_deps (blocker_record_id, blocked_record_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', '06FBKN4QVFVE51MY2N0CWCVXHM', '2026-06-12 03:16:49', '2026-06-12 03:16:49', NULL, '9063328f18ba32c0737997ac9af0911a', 2) ON CONFLICT(blocker_record_id, blocked_record_id) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_deps.updated_at OR (excluded.updated_at = ticket_deps.updated_at AND excluded.hash > ticket_deps.hash);
|
||||
@@ -0,0 +1,132 @@
|
||||
-- Auto-generated by migrate-ids (D-26). CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15.
|
||||
-- pql:created_by: migrate-ids
|
||||
-- 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);
|
||||
@@ -0,0 +1,161 @@
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62E3MNW8TBTKSR9GM', 'status', 'backlog', 'ready', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, '3fba5e751d6a8c9a29335cad1550fa0f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62E3MNW8TBTKSR9GM', 'status', 'ready', 'in_progress', NULL, '2026-04-22 09:42:11', '2026-04-22 09:42:11', '2026-04-22 09:42:11', NULL, 'c4cb4ad45703aa943016359dc46469e8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62E3MNW8TBTKSR9GM', 'status', 'review', 'done', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, '146a823b003c92bcd8b8f7e74f2f9f39', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62E3MNW8TBTKSR9GM', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:45:21', '2026-04-22 11:45:21', '2026-04-22 11:45:21', NULL, 'bbf575b4892c07075dd436e44e65b2cc', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KX7HEQJ65HZ566DG', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:12', '2026-04-22 11:46:12', '2026-04-22 11:46:12', NULL, 'f5088eafcad461a9a4ee4b4df88357d2', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KX7HEQJ65HZ566DG', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:16', '2026-04-22 11:46:16', '2026-04-22 11:46:16', NULL, '556eba472e2de76accea58ee559ce7d2', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KX7HEQJ65HZ566DG', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:26', '2026-04-22 11:46:26', '2026-04-22 11:46:26', NULL, '3a17808cf45fab2b987da0d0cbde98b6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KX7HEQJ65HZ566DG', 'status', 'review', 'done', NULL, '2026-04-22 11:46:33', '2026-04-22 11:46:33', '2026-04-22 11:46:33', NULL, '2b09b099a97c35e26f35ac37228e9411', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M1VZ8Z2SZ94F0EQR', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '0492e38c415358802970cb077447f7d7', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M1VZ8Z2SZ94F0EQR', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '1ed839ad770ebbffdf23364dbcb766eb', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M1VZ8Z2SZ94F0EQR', 'status', 'review', 'done', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, '4f52a76829285f14e71363d162c600e3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M1VZ8Z2SZ94F0EQR', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:37', '2026-04-22 11:46:37', '2026-04-22 11:46:37', NULL, 'efda926413d09bbd05a1c88275fc927f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GSR5WPC2T7CJM8XW', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '0c904f00c761b272e945045651b3c405', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GSR5WPC2T7CJM8XW', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '0f7b405a869b6c6d17bddd2189c5b52f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GSR5WPC2T7CJM8XW', 'status', 'review', 'done', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, '46d21523224743c31eb97a3c1e18d2d9', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GSR5WPC2T7CJM8XW', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:44', '2026-04-22 11:46:44', '2026-04-22 11:46:44', NULL, 'a33b066448c87f7be0542d61dfad5f66', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4XZA5399D2EHYCVFC', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '061a7151be46322c0128980c073356df', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4XZA5399D2EHYCVFC', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '08480dd43012f8f6f37e212767d26be2', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4XZA5399D2EHYCVFC', 'status', 'review', 'done', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '7c004a225b6b82840101504aef4e758c', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4XZA5399D2EHYCVFC', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:46:48', '2026-04-22 11:46:48', '2026-04-22 11:46:48', NULL, '905c9d2df79619009b56009d193aa952', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7DV4ZS010XWZJ9A84', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:46:52', '2026-04-22 11:46:52', '2026-04-22 11:46:52', NULL, '9cafe6c3de8d9f5a3db8321ac7bcbafc', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7YHYTWJGWPFJNHEAR', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '11c89804420370461fbd8fe880758f7e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM79D19C0ZTXAVPKCCG', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '36f16d47e4acf599d2d78933bcc03c77', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6TS48DVFDVJNZ5WGC', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '4162b254ba6ddb4daa8e85e428c5608c', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6TS48DVFDVJNZ5WGC', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '45a71ef6bad527950f92d9323c3edf0a', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4R0WRHSJV1MVCW31M', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '477dc88ff367b8a6bd393711a9dc934b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7YHYTWJGWPFJNHEAR', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '5d92aa65816538906b0cc1ed5ee87ed3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7YHYTWJGWPFJNHEAR', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '6b742f381bb7a072b02dfc55db62447f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6TS48DVFDVJNZ5WGC', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '9842cfa577a4dc7eaf6792266bb365be', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4R0WRHSJV1MVCW31M', 'status', 'review', 'done', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, '9dd7f0c00d8c7e3c7a67abaa8412bec9', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM79D19C0ZTXAVPKCCG', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'c557f84b4744487609e7ffb02fd2ed49', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4R0WRHSJV1MVCW31M', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'cd7a68edc46c277c49fa5839990504ad', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM79D19C0ZTXAVPKCCG', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'd387e7910739e3ecc6702b51f0d8b9e4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM79D19C0ZTXAVPKCCG', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'e1d3d1a9ca033dd1d334d478c8802e32', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7YHYTWJGWPFJNHEAR', 'status', 'in_progress', 'review', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'e4253f407304ff571299945fabe31eee', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4R0WRHSJV1MVCW31M', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'f22005f7ae58243f1fce0b07e6475235', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6TS48DVFDVJNZ5WGC', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:47:11', '2026-04-22 11:47:11', '2026-04-22 11:47:11', NULL, 'feb8a00523b7118d285847f7d88e70d3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7TW8EJNJ2VP306A20', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, '4bad0ff51702836a574ecb8afb5bd47e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QHBTA4VE6TWQT43R', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'e64eeea1f1fcbea7c020191e6c94303d', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7DV4ZS010XWZJ9A84', 'status', 'ready', 'in_progress', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'e767c59bd07f4ceb3dabbbbd9a8b25dc', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59BAJSJ1JESDTPNER', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'f32b33eaddf68e8213bfbe698960da29', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM75XRV8AQG2HEQZEJ0', 'status', 'backlog', 'ready', NULL, '2026-04-22 11:52:59', '2026-04-22 11:52:59', '2026-04-22 11:52:59', NULL, 'feb5c5a54317cafb1ca8d77ba93cc81e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM75XRV8AQG2HEQZEJ0', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '08373a38596647969ebd74504544616a', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM75XRV8AQG2HEQZEJ0', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '3a7ee18edc398147113c0ab45b4812eb', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59BAJSJ1JESDTPNER', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '5adac012531c1b1f400ecb2290782e5b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QHBTA4VE6TWQT43R', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, '7952677f8c48b6dc6e9c6e05a78e80a8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QHBTA4VE6TWQT43R', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'a545724b00ae8c2f2b9ea09cb0a22eb0', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59BAJSJ1JESDTPNER', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'ba68ce26a31c4bc0c56d5dcf1640b6e9', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59BAJSJ1JESDTPNER', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'c1a7e08caf95080e168df4e611dba0d7', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7TW8EJNJ2VP306A20', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'c97eb916069dd8267e623872edcd7452', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7TW8EJNJ2VP306A20', 'status', 'ready', 'in_progress', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'd520ce983ac294f25f96da3e22fe2257', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM75XRV8AQG2HEQZEJ0', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'db6a409471d1fcf96494fca0cf36b950', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7TW8EJNJ2VP306A20', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'eabc93b76de154cf79976608c0d00603', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QHBTA4VE6TWQT43R', 'status', 'review', 'done', NULL, '2026-04-22 12:03:48', '2026-04-22 12:03:48', '2026-04-22 12:03:48', NULL, 'ed6fc64e88ca1dc9065e72ebd6067e4d', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7DV4ZS010XWZJ9A84', 'status', 'in_progress', 'review', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '11f427298c60d6f583daa698b0b54e37', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7DV4ZS010XWZJ9A84', 'status', 'review', 'done', NULL, '2026-04-22 12:03:54', '2026-04-22 12:03:54', '2026-04-22 12:03:54', NULL, '8a18fc5ed3fbf30e4abddd0cfe100bff', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JH7WV2RH5DGEA2AC', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, '74a219dafd7350205cac9eeda53552c2', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6VMWWSP4BV8Q11W0M', 'status', 'backlog', 'ready', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'e7e1a901f6e799c0eae79c9dc2bb4a33', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JH7WV2RH5DGEA2AC', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'ec16cf36360eb2a7805bffed1a243ecf', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6VMWWSP4BV8Q11W0M', 'status', 'ready', 'in_progress', NULL, '2026-04-22 14:16:39', '2026-04-22 14:16:39', '2026-04-22 14:16:39', NULL, 'f49e07cca55f982a6a07162f55c0c6d8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6VMWWSP4BV8Q11W0M', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '127e63f3c290f90e7c3aa847afd24923', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JH7WV2RH5DGEA2AC', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '488736bebe779b1281384ce5765a883e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JH7WV2RH5DGEA2AC', 'status', 'in_progress', 'review', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '76d283c661ddda877270447bf9ed3b0b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6VMWWSP4BV8Q11W0M', 'status', 'review', 'done', NULL, '2026-04-22 14:30:05', '2026-04-22 14:30:05', '2026-04-22 14:30:05', NULL, '8108195e0776ff2e2493720ef80c5b6b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6PMW57C70KBJVAV10', 'status', 'backlog', 'cancelled', NULL, '2026-04-22 20:34:17', '2026-04-22 20:34:17', '2026-04-22 20:34:17', NULL, '66eac257c948b529c5949d17ca5f1d11', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4VVHNKNXPYD4XWRYW', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, '2031b1358057b8fad868649bb7484500', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GHHRANK5532VJVJR', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'a46451df6d3b67e3bcfbf48bb73c2e12', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM64DC28XV20JJ8KW00', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'c397fe7de79cc82035f57a5cdaa2b83d', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5WXV1RKPDK810C3FR', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:38:42', '2026-04-22 20:38:42', '2026-04-22 20:38:42', NULL, 'd471b281adf859311624bb19452d36ba', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5WXV1RKPDK810C3FR', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '154c84814ce1eacfd09580762e35748e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4VVHNKNXPYD4XWRYW', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, '2a3c5b0f2799072f0c593128ac99ddec', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM64DC28XV20JJ8KW00', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, 'e78fe6d9079f625573cf0993bb73a684', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GHHRANK5532VJVJR', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:38:49', '2026-04-22 20:38:49', '2026-04-22 20:38:49', NULL, 'ffade557a91afb2ce8d223a59c987d15', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GHHRANK5532VJVJR', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '0ad715f5bfdec4a3c8005f0f4cb857d6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4VVHNKNXPYD4XWRYW', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '4cd3af18c76884911ac0eff87f93e4c0', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5WXV1RKPDK810C3FR', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '870fda31d4fbd8c39e4a6ed78551a089', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM64DC28XV20JJ8KW00', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:38:51', '2026-04-22 20:38:51', '2026-04-22 20:38:51', NULL, '92206091799f820dd2ae2778a92947fc', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4005W4QV0ZPBMQ02C', 'status', 'in_progress', 'done', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '0798cec4e8ce91238498218a90fc2b4e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4005W4QV0ZPBMQ02C', 'status', 'backlog', 'ready', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '4d677f10ac70a801bc5d691e618dd098', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4005W4QV0ZPBMQ02C', 'status', 'ready', 'in_progress', NULL, '2026-04-22 20:56:58', '2026-04-22 20:56:58', '2026-04-22 20:56:58', NULL, '79fde318878ec91f2cd020be9db706c4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7MKCHC37G69SJGQ04', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '615893bb4159bca57088c5a8e202e834', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7MKCHC37G69SJGQ04', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, '7350f6584bee9eeadb6a41001f34876b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7MKCHC37G69SJGQ04', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:02:29', '2026-04-22 21:02:29', '2026-04-22 21:02:29', NULL, 'a4994cbb075a8921e4d0d505d922981c', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KH7VR0BMZV5SSTXC', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '114825918ccc9039ad4653dc5c3b5785', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6JXWYYQ6MT6N8STYM', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '1bc4adcc9db4ab023130198ed8299bb4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5Q6AWSTKGT0P64TRR', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '2c6e445be22794878bd6784a5973c028', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KH7VR0BMZV5SSTXC', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '307d5b39c6fe3df3d2a620a591d7d5c8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5Q6AWSTKGT0P64TRR', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '798c86f77cdd2b3a1a337291215b45d6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6JXWYYQ6MT6N8STYM', 'status', 'backlog', 'ready', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '81141b1ca5d1d6ba526b33054189ebfa', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6JXWYYQ6MT6N8STYM', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '83e47e7c7550ce0f3c724edfa773016f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KH7VR0BMZV5SSTXC', 'status', 'in_progress', 'done', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, '8b6ef7ffc9b872edbe8ccd513f0a239e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5Q6AWSTKGT0P64TRR', 'status', 'ready', 'in_progress', NULL, '2026-04-22 21:16:41', '2026-04-22 21:16:41', '2026-04-22 21:16:41', NULL, 'c040aca52959ae8febda5e0dd54252a2', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QHBTA4VE6TWQT43R', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '007710b30c9e75b030b44dba9fa5f824', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM79D19C0ZTXAVPKCCG', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '0c80d9ff14fd5992b92f0c9b8b9eb647', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6VMWWSP4BV8Q11W0M', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '18e5b09f781d974d4746fa984734872f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67JSC5RKS6M9182KG', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '1fbe35313558f6277b818cff665009a9', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YG22EV7BFTX5RTPR', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '20d5c46249f5b8810e17914929ec763e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM699HAWC76SHRA7AM4', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '251327841ab9971717b36c2cb79ca299', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7TW8EJNJ2VP306A20', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '2e3916450f98a766aac0c2abc301e6c4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5E41Q8TX8X55X4MZ0', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '4502258c81a7e75eafd3631e0cdf90b8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67ZXPWX71ND4C699C', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '4c190ec1739ed8ec68fe694687fa5fe3', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'parent_id', NULL, 'T-7', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '4ca23526443704936c352c2842f2f7f8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5KYAV8TMH3Y3FPRSR', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '50dc074f7f06d51e11fee8d00dd7dd74', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7YHYTWJGWPFJNHEAR', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '6a700a972bd1c1a0729bf27078def56f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5Q6AWSTKGT0P64TRR', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '773e27abfec30eeb016b441c1dc0d8f8', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM75XRV8AQG2HEQZEJ0', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '881064c3f6f63638a754929f1078b011', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9404bff31ca5c310db47f5c0e11c90ca', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48MDE8ZZ82VWNY994', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '96cf4f78cd06636178ba9a64d6b83f2d', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4R0WRHSJV1MVCW31M', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9b17afe084710d3e198c7435ee0fb4b6', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6RDM2EKZX132GPV7M', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9ca8eb0c58afc81c07a7538b8e504ad4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66TK8TQ1X5T9GK6V0', 'parent_id', NULL, 'T-5', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, '9f69eaa0855cbea3a8e715b7bb917624', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6PMW57C70KBJVAV10', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'b683b61fdb656a5f6df871190736771f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KH7VR0BMZV5SSTXC', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'c27a712580912bc125013ca9ee3330a1', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4005W4QV0ZPBMQ02C', 'parent_id', NULL, 'T-4', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'cca49092a5f8f1912acd35c168101148', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4M63G18VAQFG6R3XW', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ce9352bab1e00b12f8ff794fca067732', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62E3MNW8TBTKSR9GM', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'dc8745c0d7b3445b0c78985f1f9995dc', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6TS48DVFDVJNZ5WGC', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'e1831cbdaa2e3ce0000a590874c08a76', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6JXWYYQ6MT6N8STYM', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'e5214fde5b73b4a7165107dc85057ec5', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JH7WV2RH5DGEA2AC', 'parent_id', NULL, 'T-2', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'e6649095b63a9a991df2db00a7ad53ca', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4W194B2421P2SF83R', 'parent_id', NULL, 'T-8', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'ee6e9785f9f760ab2c6b3f689afefad2', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5QHMB46NSHHPVQRF0', 'parent_id', NULL, 'T-3', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'f9b20bf38afe42bfdfd811ab282658ee', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59BAJSJ1JESDTPNER', 'parent_id', NULL, 'T-6', NULL, '2026-04-23 10:23:49', '2026-04-23 10:23:49', '2026-04-23 10:23:49', NULL, 'fb24ffd20df49eb6febd496a6c1c02bf', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, '728ad0916172fb20a1c4b19d0753e4db', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:01:08', '2026-04-24 07:01:08', '2026-04-24 07:01:08', NULL, 'bfd785a9e40e27979d287ec137ec01d1', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, '3b1ea2037fa8d26c4293a2bd5d0fa007', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:01:48', '2026-04-24 07:01:48', '2026-04-24 07:01:48', NULL, 'd5329907e937e55e00a9d002da5ac6d4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:40', '2026-04-24 07:05:40', '2026-04-24 07:05:40', NULL, '5a372aa4814812aa716d853550fea046', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:05:50', '2026-04-24 07:05:50', '2026-04-24 07:05:50', NULL, 'ba35f95ff2763f26ebccff21a2b826af', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:05:51', '2026-04-24 07:05:51', '2026-04-24 07:05:51', NULL, '4e46180f02c86278deedd9f8f08d4d07', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:06:30', '2026-04-24 07:06:30', '2026-04-24 07:06:30', NULL, '8e985069c3c5eeb6b172f83430458810', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:06:32', '2026-04-24 07:06:32', '2026-04-24 07:06:32', NULL, '66c14397b8af87bceaace31b1dff2853', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'review', 'done', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '5733cc23693ca40190bd8c5063a7a022', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'in_progress', 'review', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, '9669c3ab67cb4477961d2ee3aa799a90', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, 'de6f0dd22f5f0e664793f74429322b8b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:07:55', '2026-04-24 07:07:55', '2026-04-24 07:07:55', NULL, 'e7b5b2d4e43d09ede3d177528718683a', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:00', '2026-04-24 07:08:00', '2026-04-24 07:08:00', NULL, '976f8fecced9b5322ed3403b8fbdf0dc', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'in_progress', 'done', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, '281fe02b5f9467e50e0a186fc1afb77c', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'done', 'in_progress', NULL, '2026-04-24 07:08:29', '2026-04-24 07:08:29', '2026-04-24 07:08:29', NULL, 'fd27037a9f94cf0df10016ff2c3b6e78', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, '52ed48f7463c234efeb35fcf45957e8a', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:35', '2026-04-24 07:08:35', '2026-04-24 07:08:35', NULL, '8fb22a82912e93b9abff0da33d371093', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'backlog', 'cancelled', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '3e65f78d186b37308165679295b40408', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'cancelled', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '5b81f1c8048cd2b3ef1055ac9b04f958', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'ready', 'backlog', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '7348283db8af7104b475d0d84a0a7157', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'in_progress', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '804f233159ce47a2c213778971fc00a1', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'backlog', 'ready', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, '91171fb011294517240bd33360b08119', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'ready', 'in_progress', NULL, '2026-04-24 07:08:59', '2026-04-24 07:08:59', '2026-04-24 07:08:59', NULL, 'd0b87332faf5a67e7e6d36ff9c981c00', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'backlog', 'done', NULL, '2026-04-24 07:30:15', '2026-04-24 07:30:15', '2026-04-24 07:30:15', NULL, 'bf086ebf2d4e6b0a592f8386e916115b', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'status', 'done', 'backlog', NULL, '2026-04-24 07:30:22', '2026-04-24 07:30:22', '2026-04-24 07:30:22', NULL, '8e29783cc997d80b6699bd14537e8613', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, '1bc6c7c959742697cd8670771ebeb8d9', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:08:16', '2026-04-24 08:08:16', '2026-04-24 08:08:16', NULL, '51a117e3e688d86d72e33a844dfb6217', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:23', '2026-04-24 08:08:23', '2026-04-24 08:08:23', NULL, 'bf1f3a13e6bc7df6adf20b545e6911aa', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:08:38', '2026-04-24 08:08:38', '2026-04-24 08:08:38', NULL, '66d52940767ba7c91f848431ee618ba1', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:08:40', '2026-04-24 08:08:40', '2026-04-24 08:08:40', NULL, '1154f797d2c87e3e5aff6c8407b97f7e', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'in_progress', 'review', NULL, '2026-04-24 08:08:44', '2026-04-24 08:08:44', '2026-04-24 08:08:44', NULL, '99a8c33d31f16025ad3a27305ec703a0', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'review', 'in_progress', NULL, '2026-04-24 08:08:48', '2026-04-24 08:08:48', '2026-04-24 08:08:48', NULL, '9475c2583b2f0d3742f792f5dcffd4aa', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'in_progress', 'ready', NULL, '2026-04-24 08:16:15', '2026-04-24 08:16:15', '2026-04-24 08:16:15', NULL, 'df08122765cc76b0003b6e9ff54426fb', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'ready', 'backlog', NULL, '2026-04-24 08:16:19', '2026-04-24 08:16:19', '2026-04-24 08:16:19', NULL, '4e273f400ffcfbf2bc926ee4e9575fbc', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:16:21', '2026-04-24 08:16:21', '2026-04-24 08:16:21', NULL, '6e750d5ebacc51f2734e94675322e5c4', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'status', 'ready', 'in_progress', NULL, '2026-04-24 08:16:47', '2026-04-24 08:16:47', '2026-04-24 08:16:47', NULL, '9e82a53ad5b5d58037280a6778e335fe', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4M63G18VAQFG6R3XW', 'status', 'backlog', 'in_progress', NULL, '2026-04-24 08:16:56', '2026-04-24 08:16:56', '2026-04-24 08:16:56', NULL, '8a9247ea1de65f4696dc7af64a5d2b7f', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
INSERT INTO ticket_history (ticket_record_id, field, old_value, new_value, changed_by, changed_at, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67ZXPWX71ND4C699C', 'status', 'backlog', 'ready', NULL, '2026-04-24 08:17:11', '2026-04-24 08:17:11', '2026-04-24 08:17:11', NULL, '38b265daee03a4df5fb525e48b477388', 2) ON CONFLICT(hash) DO NOTHING;
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,132 @@
|
||||
-- Auto-generated by migrate-ids (D-26). CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15.
|
||||
-- pql:created_by: migrate-ids
|
||||
-- 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);
|
||||
@@ -0,0 +1,173 @@
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67ZXPWX71ND4C699C', 'T-43', '2026-04-22 22:01:13', '2026-05-07 19:04:55', NULL, '069521bcf068ff6889cee5251511446c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7SS2V5Z1M1XJGQX0M', 'T-86', '2026-05-06 13:28:58', '2026-05-07 19:04:55', NULL, '07e51e80b6df55240c465d17a9430700', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7P6Q0DG4RG4CBHFJG', 'T-67', '2026-04-24 06:34:16', '2026-05-07 19:04:55', NULL, '0b64bd846488ccf4fce74a6e14629edb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM460GRBMZMV8E5339M', 'T-77', '2026-05-05 12:58:59', '2026-05-07 19:04:55', NULL, '0cdd07e0992ab8eb76d6945ae1079855', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM79D19C0ZTXAVPKCCG', 'T-12', '2026-04-22 11:47:03', '2026-05-07 19:04:55', NULL, '10055ea4ae6c926286eb81eb35b8ef8f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427FX3GTR3VNNKM40', 'T-63', '2026-04-24 06:34:16', '2026-05-07 19:04:55', NULL, '178d61b166cf7e8f17efc83cdf60c062', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7DV4ZS010XWZJ9A84', 'T-6', '2026-04-22 11:45:41', '2026-05-07 19:04:55', NULL, '17e7bcab28adfbab47be2ad9aa4e4c56', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4W194B2421P2SF83R', 'T-25', '2026-04-22 20:14:24', '2026-05-07 19:04:55', NULL, '1b888a483ec795737d7a1cf71fdbd699', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM77477EN2R5872G95M', 'T-93', '2026-05-07 06:33:47', '2026-05-07 19:04:55', NULL, '1f5f4e360943d2a903cdd7c819bfeaa3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7D8AG5AY1CW58HCB4', 'T-58', '2026-04-23 20:32:06', '2026-05-07 19:04:55', NULL, '253f8fb1458c5394eea856c885f94fff', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4M63G18VAQFG6R3XW', 'T-28', '2026-04-22 20:14:52', '2026-05-07 19:04:55', NULL, '280276cb372707c0fcdbc7de5c68f568', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7CEP425DNV3W7RFPM', 'T-72', '2026-05-05 12:53:05', '2026-05-07 19:04:55', NULL, '2c7cd24dee2e7d21a31f476af233284d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4XZA5399D2EHYCVFC', 'T-5', '2026-04-22 11:45:41', '2026-05-07 19:04:55', NULL, '31b2ad13bb8bcc41e4d21825a232f086', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM79CNBXJ2S3CFQR7VM', 'T-81', '2026-05-05 12:58:59', '2026-05-07 19:04:55', NULL, '32698f648acf611d10fc719f6be28bad', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM76XDA3SZMPWR8WQSC', 'T-62', '2026-04-24 06:34:16', '2026-05-07 19:04:55', NULL, '3439d016919d87e8b4860735a23de1a8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GSR5WPC2T7CJM8XW', 'T-4', '2026-04-22 11:45:41', '2026-05-07 19:04:55', NULL, '407e3921ca9e6d5d13e11b7f577ad591', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4PMYC2E1X99HX4MDM', 'T-84', '2026-05-06 10:16:02', '2026-05-07 19:04:55', NULL, '40ce052d36bd0270aa01f9e25c1a944a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5A8DYZ3BCJN1DEY7C', 'T-71', '2026-05-05 12:53:01', '2026-05-07 19:04:55', NULL, '43cfd9109375b0fb6a8f176a0df764a5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48MDE8ZZ82VWNY994', 'T-27', '2026-04-22 20:14:45', '2026-05-07 19:04:55', NULL, '440474d801f1b9bd05e8fbb4254c3817', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M1VZ8Z2SZ94F0EQR', 'T-3', '2026-04-22 11:45:41', '2026-05-07 19:04:55', NULL, '44d2e24e49ca8555a6b3327f7e954ede', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7AVABFH067XGTTCVM', 'T-76', '2026-05-05 12:58:59', '2026-05-07 19:04:55', NULL, '4a3e92a591238f629d18f9b10087c7f6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5WXV1RKPDK810C3FR', 'T-31', '2026-04-22 20:33:56', '2026-05-07 19:04:55', NULL, '4c423ea86e8173f7fa6edddea9adbadb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62E3MNW8TBTKSR9GM', 'T-1', '2026-04-22 09:41:51', '2026-05-07 19:04:55', NULL, '55239329122e1f84e9aeac9cee14a21a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KH7VR0BMZV5SSTXC', 'T-37', '2026-04-22 21:03:40', '2026-05-07 19:04:55', NULL, '55c80e1ce022b96aab6e97e31579c302', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42RQZVEWTF9CJSRF8', 'T-85', '2026-05-06 13:28:58', '2026-05-07 19:04:55', NULL, '6133918ee124e5502286fa0de06b5bd9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7QREFG3SBG2QPYY7R', 'T-59', '2026-04-23 20:32:06', '2026-05-07 19:04:55', NULL, '64c389eab795606b9d8b5b65877a67bc', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JH7WV2RH5DGEA2AC', 'T-19', '2026-04-22 14:08:40', '2026-05-07 19:04:55', NULL, '65ba2e99100be29287532b3390054401', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YG22EV7BFTX5RTPR', 'T-26', '2026-04-22 20:14:33', '2026-05-07 19:04:55', NULL, '685c38f471288372eab298a106a06d66', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6T6580D8ABDVTMNZW', 'T-8', '2026-04-22 11:45:41', '2026-05-07 19:04:55', NULL, '6aa0a08f23546d11b37f80d54920c3be', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4VVHNKNXPYD4XWRYW', 'T-30', '2026-04-22 20:33:53', '2026-05-07 19:04:55', NULL, '6d929d2dcc64310a43b21be1a8314258', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4TQEYESK545T8F164', 'T-57', '2026-04-23 20:32:06', '2026-05-07 19:04:55', NULL, '6ff368c440149011af6cc3e2bdfe243f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM79H4MPEWW2TWQ89D0', 'T-46', '2026-04-23 20:27:58', '2026-05-07 19:04:55', NULL, '71f030d09b588292801ae8853accd90c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM70QNVQY1MQGP3A33G', 'T-78', '2026-05-05 12:58:59', '2026-05-07 19:04:55', NULL, '73ecd48d2d9f1cdf8e1bf6e88a8dc812', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM52JT96V5067SM0DMR', 'T-87', '2026-05-06 13:46:07', '2026-05-07 19:04:55', NULL, '740c1dcab17dbdc339750aa8e32a1281', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QKBQY7FPCNX6N28R', 'T-45', '2026-04-23 20:27:28', '2026-05-07 19:04:55', NULL, '7c312190b902337039b21f4743be8908', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5Q6AWSTKGT0P64TRR', 'T-39', '2026-04-22 21:03:40', '2026-05-07 19:04:55', NULL, '825042c21d032181c1fe6d55a29a94f9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59BAJSJ1JESDTPNER', 'T-15', '2026-04-22 11:47:18', '2026-05-07 19:04:55', NULL, '836e6112c6e1dff23772619499fad245', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7TW8EJNJ2VP306A20', 'T-14', '2026-04-22 11:47:18', '2026-05-07 19:04:55', NULL, '8496d923db0e1fa76e7b787a273e974e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5QHMB46NSHHPVQRF0', 'T-24', '2026-04-22 14:08:40', '2026-05-07 19:04:55', NULL, '8a892f3717f8da2c93f67d3170af014f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5AACHWPFWZDCJ3CPW', 'T-73', '2026-05-05 12:53:21', '2026-05-07 19:04:55', NULL, '8e532488915724b80881c7b2dc227de9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P00AT6KGVZ1EF60C', 'T-18', '2026-04-22 13:26:29', '2026-05-07 19:04:55', NULL, '900d00f28a29207a464017a0bd750f33', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM734YZ060Q63H40EYG', 'T-80', '2026-05-05 12:58:59', '2026-05-07 19:04:55', NULL, '90da3a2c5bd03b314d661362a3919506', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6JXWYYQ6MT6N8STYM', 'T-38', '2026-04-22 21:03:40', '2026-05-07 19:04:55', NULL, '91c3ef42d94e16789915ee6366f7015d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM75XRV8AQG2HEQZEJ0', 'T-16', '2026-04-22 11:47:18', '2026-05-07 19:04:55', NULL, '98bf61f1693e8b8877c2cc03c9fb120d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67JSC5RKS6M9182KG', 'T-41', '2026-04-22 21:03:40', '2026-05-07 19:04:55', NULL, '9ab8f6ffa933c87dd3153eaea452e902', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66FTCTWHH9AQTNFKR', 'T-55', '2026-04-23 20:32:06', '2026-05-07 19:04:55', NULL, 'a7b7dc2af0426c8bcbfea6a3023a0aff', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7YHYTWJGWPFJNHEAR', 'T-10', '2026-04-22 11:47:02', '2026-05-07 19:04:55', NULL, 'a8fa7598a624ad83878db046ffa8986f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6TS48DVFDVJNZ5WGC', 'T-11', '2026-04-22 11:47:02', '2026-05-07 19:04:55', NULL, 'af42494291be5910520becc10285e157', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4R0WRHSJV1MVCW31M', 'T-9', '2026-04-22 11:47:02', '2026-05-07 19:04:55', NULL, 'b8d2fca6e4b3882e4cd1ec486924265b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4005W4QV0ZPBMQ02C', 'T-35', '2026-04-22 20:34:06', '2026-05-07 19:04:55', NULL, 'bbbe9cce785ce60416cba671cea2985a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50DVPFP4WY74RQBB8', 'T-61', '2026-04-24 06:34:16', '2026-05-07 19:04:55', NULL, 'bbf53c20ed761296c7bd54d418b67218', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50X0DX8XTVZEA5GN8', 'T-60', '2026-04-23 20:32:06', '2026-05-07 19:04:55', NULL, 'bc1502a8c8fe4e44c004f56e9e15b03e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6PMW57C70KBJVAV10', 'T-22', '2026-04-22 14:08:40', '2026-05-07 19:04:55', NULL, 'be0ae61f02897da33e8a9b2d6d1b002e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GHHRANK5532VJVJR', 'T-34', '2026-04-22 20:34:03', '2026-05-07 19:04:55', NULL, 'c042cabf5814a596e1582a8a85138543', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM606BMYDDGRSJBVPG8', 'T-70', '2026-05-05 12:52:56', '2026-05-07 19:04:55', NULL, 'c04d59ecdcc1ade801ed7f0866a3f00c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM74S58G8XZV1766VD0', 'T-90', '2026-05-06 20:38:14', '2026-05-07 19:04:55', NULL, 'c36ec6ce6ac423fa9983758304139d2b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7CEKQCMZAV751402G', 'T-47', '2026-04-23 20:28:43', '2026-05-07 19:04:55', NULL, 'c5abd99aac954b783b84f3b77d003691', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5EP8B65T0AR52TQFW', 'T-92', '2026-05-07 05:51:48', '2026-05-07 19:04:55', NULL, 'c62020a1cf82eb7ed6bb5635d5f8f0fa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QHBTA4VE6TWQT43R', 'T-13', '2026-04-22 11:47:18', '2026-05-07 19:04:55', NULL, 'd08af226653f0822b332b640dfcedde5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KX7HEQJ65HZ566DG', 'T-2', '2026-04-22 11:45:26', '2026-05-07 19:04:55', NULL, 'd1b1abb9046e230f581009daf61e7bfd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7Y0DFP66HSP073XHC', 'T-88', '2026-05-06 14:34:35', '2026-05-07 19:04:55', NULL, 'd1ea1e789ff487787076d0ca66670e35', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JZ2AKN74FKMK3MR8', 'T-69', '2026-05-05 12:52:52', '2026-05-07 19:04:55', NULL, 'd2622779cfbab03758ed02949f12fa72', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7MKCHC37G69SJGQ04', 'T-32', '2026-04-22 20:33:58', '2026-05-07 19:04:55', NULL, 'd4125c549e70d4f4d37bb43a4f3ccbd1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NS39QCDHNVVVT1R4', 'T-17', '2026-04-22 13:26:29', '2026-05-07 19:04:55', NULL, 'd732affde22ee738f13bacb127c094e7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5X2K66A0MQY2VZ7H4', 'T-83', '2026-05-06 09:17:39', '2026-05-07 19:04:55', NULL, 'da553a2c61adb1795c0ab33867dc6ba3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4Q4XW93E3WA1SD9KW', 'T-75', '2026-05-05 12:58:59', '2026-05-07 19:04:55', NULL, 'db0b7c934e54d06fcf33aaf168a723e4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6YBQP75NQQ4Y9DN9C', 'T-68', '2026-05-05 12:52:48', '2026-05-07 19:04:55', NULL, 'e14345ea70ff466ed3d9b8de161ea556', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6VMWWSP4BV8Q11W0M', 'T-20', '2026-04-22 14:08:40', '2026-05-07 19:04:55', NULL, 'ebf54efaf812fb838959a80b4cdcc527', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM699HAWC76SHRA7AM4', 'T-21', '2026-04-22 14:08:40', '2026-05-07 19:04:55', NULL, 'f217b0ca72240fbbe463d296f250f3b6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM64DC28XV20JJ8KW00', 'T-33', '2026-04-22 20:34:01', '2026-05-07 19:04:55', NULL, 'f7924a89ac74721c9145a511b7a1b4e9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM454GCK8ENRKRA44NW', 'T-49', '2026-04-23 20:31:35', '2026-05-07 19:04:55', NULL, 'f9fb75eb241d8edd62688d7ac3fdd25a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM57AEVK5E0GWVPGAY8', 'T-79', '2026-05-05 12:58:59', '2026-05-07 19:04:55', NULL, 'fe4c71ca925995617dcd4409caaae9d0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6RDM2EKZX132GPV7M', 'T-40', '2026-04-22 21:03:40', '2026-05-07 19:04:55', NULL, 'fe57a623da7c3fda11455f1d78ba7f4c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7162Z26FDWXG35VH0', 'T-7', '2026-04-22 11:45:41', '2026-05-07 19:04:55', NULL, 'ffa3d6a93f30550da97a3357a0b40c3c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48X0763Z9DZSM0HFR', 'T-94', '2026-05-07 19:08:29', '2026-05-07 19:08:52', NULL, '8087c73fd647b2156a0275ec7ec2ffcd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5WCDTR41VN9P1EGMC', 'T-96', '2026-05-17 17:16:01', '2026-05-17 18:11:18', NULL, 'c44d6bedd77f53875b4fb7f92e8c9895', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F3V130BQXQH6HDVC', 'T-98', '2026-05-17 18:47:13', '2026-05-17 18:55:57', NULL, 'b2543702670de76b92f98bcb3690901e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7NRKR9J1QTDSP9650', 'T-101', '2026-05-17 18:47:29', '2026-05-17 19:01:39', NULL, '8acbd73697594b2f0686c7e1f01f0b8e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QNZM15V1ZK32YS6G', 'T-102', '2026-05-17 18:47:33', '2026-05-17 19:01:39', NULL, 'c77e3fe23ababa8854065860aaaf7645', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM493QAZNJTJQHYFGCC', 'T-103', '2026-05-17 18:47:39', '2026-05-17 19:18:43', NULL, '34fae6a3452e2f3a6fc76605fb82cd85', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6MP9JD93ZE0CDGFFW', 'T-106', '2026-05-17 18:47:51', '2026-05-17 19:18:43', NULL, '52a5c071f2c90428eba13aaa8f785831', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7WW3008NZRJWJ88HG', 'T-113', '2026-05-17 18:48:24', '2026-05-17 19:18:43', NULL, 'fabd5c2006ae0e13714b1aa12ecd2e41', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6MWAKNB2NE3ZZ0XKW', 'T-110', '2026-05-17 18:48:09', '2026-05-17 19:39:12', NULL, '22d75188f931b18801e8e9015e9863a5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM60QRRNEEWG84VWKXC', 'T-66', '2026-04-24 06:34:16', '2026-05-17 19:39:37', NULL, '8b24910aba2e49638668b03f64006a5a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42M9RK4399B4F4WSG', 'T-64', '2026-04-24 06:34:16', '2026-05-17 19:39:37', NULL, '9d3e8f627b3095e1db58e047618be710', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM65SSVGN5M36T4QNDC', 'T-117', '2026-05-17 19:28:28', '2026-05-17 19:39:40', NULL, '852007cdda3b405757c088e160798b40', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4CWANNSKNF2X5JEJ4', 'T-100', '2026-05-17 18:47:23', '2026-05-17 19:47:50', NULL, '3edd0e73ac89116392d4aab4e8180c6a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63H3CXCKB13E3RNHW', 'T-105', '2026-05-17 18:47:47', '2026-05-17 19:56:28', NULL, '0f673c39dc8c450947f34c86e743f8cc', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM40PV7G53JMHHNYPYG', 'T-108', '2026-05-17 18:47:59', '2026-05-17 20:05:38', NULL, 'f023344206a267eefd5d2c453d3483c9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4ZNR15BSANVX07N1R', 'T-112', '2026-05-17 18:48:20', '2026-05-17 20:11:16', NULL, '874a9bbe234acb55dfc20d72a2c5a361', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4W20PVMHX1BBJ4TVW', 'T-118', '2026-05-17 20:51:47', '2026-05-17 20:53:41', NULL, 'b8fbc53c7092e7b29a869f0247cd0e38', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7FG92QKNFJSCAP888', 'T-114', '2026-05-17 18:48:28', '2026-05-18 07:04:38', NULL, '2c16b22992624d00be14801c105350c9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7Y9KHNVRZ41VYW1YM', 'T-111', '2026-05-17 18:48:14', '2026-05-18 07:43:45', NULL, '1c12b7ea3d82ff2bc1e5680690ee0746', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5W67PPRVA8SAJXHFG', 'T-104', '2026-05-17 18:47:42', '2026-05-18 07:53:10', NULL, 'ec883c703bad46f757d38057c71336d3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4TTAC53CQYWDQ91H0', 'T-109', '2026-05-17 18:48:03', '2026-05-18 08:07:23', NULL, 'ed187720e362af1ff3f8cae3fd0ac828', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45DS16DX0NKMN9Q3G', 'T-121', '2026-05-18 08:07:23', '2026-05-18 08:22:19', NULL, 'ec1cbea6674c6fb5d365510587c082ec', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM614M7ES54V3SETAQM', 'T-115', '2026-05-17 19:10:21', '2026-05-18 09:30:59', NULL, '640c8f8e5a67cdb44eaabd65bf21fc04', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4RJ9K19P5AX14RHRR', 'T-123', '2026-05-18 10:29:02', '2026-05-18 10:29:02', NULL, '255468e265f300a234e8456d3a0abf51', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7FXS713CKYDZ284NC', 'T-107', '2026-05-17 18:47:55', '2026-05-18 10:30:30', NULL, '4bc77250919cd4cef2fb1bfd3438a7c6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4H5C2Z6GCBCQ85QGR', 'T-116', '2026-05-17 19:14:31', '2026-05-18 11:22:46', NULL, '972bf9cfde6de9ff37cb5cb1dcc5a2dd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM53N56Z60B4SQ5VTRG', 'T-74', '2026-05-05 12:53:22', '2026-05-18 11:51:40', NULL, '7b71c20018634be865ab843900b93eff', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AV8YXV4NYWS2ZQYW', 'T-124', '2026-05-18 11:58:47', '2026-05-18 12:51:08', NULL, 'b272f7b62db06897d71bcee86b0aef6e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KY9JVRKF22GGFMXR', 'T-125', '2026-05-18 11:58:52', '2026-05-18 15:51:09', NULL, '801eba2049b8dc046ab81f731d0caef1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78E2NAM4YK3A2AV6R', 'T-126', '2026-05-18 11:58:57', '2026-05-18 16:04:29', NULL, '6bc6a6b1f009225521ace82a6616244c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7J40S0A8Q8PC0PDY8', 'T-127', '2026-05-18 11:59:02', '2026-05-19 10:03:26', NULL, '0e2e8e90772e11cfad16ee7805a58f7e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KEY6593FMW8Y40VC', 'T-128', '2026-05-18 11:59:06', '2026-05-19 10:06:46', NULL, '9cf26209ff598d91824d59f791b34938', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM40XWGSN99DBDQDHJR', 'T-129', '2026-05-18 11:59:11', '2026-05-19 12:19:51', NULL, '618bff52db127ed5655c78c4e09f3078', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5MW2V3QCMWKMY4VQM', 'T-130', '2026-05-18 11:59:17', '2026-05-19 12:33:52', NULL, 'ab53ef037d31eb5a977cf6fe3280e126', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5ZSRZARG7SXCC71NM', 'T-131', '2026-05-18 11:59:23', '2026-05-19 13:14:41', NULL, '7339bc2840f028e45e798e8bd8c9c337', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM76Q97D8R01CRTTA9R', 'T-99', '2026-05-17 18:47:18', '2026-05-19 13:14:41', NULL, 'ff7b3f5bbb802d081c305d32b3330163', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54DENVAR46RXCCQQM', 'T-119', '2026-05-18 07:43:50', '2026-05-20 07:00:06', NULL, 'd2732a32ffbc29ddca7f9eacad2d5570', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM47ARMYK2H24KJ0DAG', 'T-120', '2026-05-18 07:53:08', '2026-05-20 16:02:35', NULL, 'fdab02033af4c3f6df800ddf3bc78198', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM56CF9W94FSFDSBKGW', 'T-97', '2026-05-17 18:47:08', '2026-05-20 16:02:46', NULL, '84c961d85f80b8ba21fc227f7d191948', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6TNXTJCQBH9VADC4M', 'T-89', '2026-05-06 20:36:42', '2026-05-22 13:37:46', NULL, '7197034866b3116490b8633fd18ff01f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4HGK6KR0314P2M32M', 'T-91', '2026-05-06 20:49:47', '2026-05-22 13:37:46', NULL, 'c1d096207b720c77ac05b1950dce1bcd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54N0ZDJRS7G6MP1XC', 'T-136', '2026-05-22 15:59:34', '2026-05-22 17:37:23', NULL, '24eae70edc48f10e227710ee77e94fb6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54G017Y29APWJTCN0', 'T-133', '2026-05-22 15:50:06', '2026-05-22 17:37:23', NULL, 'a4886b596be68f26bcf01315f2eba159', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6SRMD12DMXCX6N7DM', 'T-135', '2026-05-22 15:59:34', '2026-05-22 17:37:23', NULL, 'e18b6b13a86b6202afb93b4bae32c824', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4EQAERS4J9X9RAETR', 'T-134', '2026-05-22 15:59:34', '2026-05-22 17:55:11', NULL, '99fdef2c143698cd8dee33e5903015f5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6E3X5WWRS09GCDGK8', 'T-137', '2026-05-22 15:59:34', '2026-05-22 18:14:32', NULL, '0ae6c36aab9db692afc7b9de9d83b70b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4V1PZ0EK6G60699EM', 'T-138', '2026-05-22 15:59:34', '2026-05-22 21:49:09', NULL, '006e8c34ed9058294d4144ed2fd853ba', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427ZFSTWJ21EBK5FG', 'T-142', '2026-05-22 22:06:59', '2026-05-22 22:18:02', NULL, '93352b90d5b8532dca21609a5e6dee2c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6YWAC8SSGQBK31T98', 'T-143', '2026-05-22 22:31:22', '2026-05-22 22:36:09', NULL, 'd585cc1215d301735c9e207e08193e6e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BV0CXWCT04Z81EG0', 'T-144', '2026-05-22 22:40:42', '2026-05-22 22:43:45', NULL, 'b8e257b9abde9da0c38fc15cb7d4ee41', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM653J6604KYDK6RT8R', 'T-139', '2026-05-22 15:59:34', '2026-05-22 23:16:28', NULL, 'e43a27d4464ce09179016a65dc702a57', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5AVSMKD15KBJYX48W', 'T-140', '2026-05-22 15:59:34', '2026-05-22 23:31:46', NULL, '2e962cad8956dff339a9c3028ff8c83a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM558PCHET90BBSHGTC', 'T-146', '2026-05-23 06:19:51', '2026-05-23 06:30:47', NULL, 'd575cf325d7b347a88ca11f516b1b715', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM47FEEMHPE8J5VVBKM', 'T-147', '2026-05-23 07:03:18', '2026-05-23 07:06:54', NULL, '2b5e6b5f67000bea0097ea86986124b6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GDS1R8H2TW12WX7G', 'T-149', '2026-05-23 07:14:36', '2026-05-23 07:27:32', NULL, '1ffa4dc9299a04293bfa473036ed8e08', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5KN50C7A723SEKZXG', 'T-145', '2026-05-23 06:07:59', '2026-05-23 08:43:19', NULL, 'e688de6397028cc53baef88f4af4db5c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6F9XA7F2BX9P18BR4', 'T-150', '2026-05-23 09:04:40', '2026-05-23 09:33:03', NULL, '6103463406709c57456e06226486a739', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM77GWREHKA9K45P020', 'T-151', '2026-05-23 09:52:14', '2026-05-23 10:20:12', NULL, '1ee4e62532962562e7a57316eedf0d04', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7BJS5E1JJGSJ9XHG0', 'T-153', '2026-05-23 09:52:27', '2026-05-23 10:35:56', NULL, '872e6c5e27120a76b24f7c033286071e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6YSQYCE096R2SFRXG', 'T-152', '2026-05-23 09:52:20', '2026-05-23 10:43:52', NULL, '9ba6f625fd14bb95a31902b699e821e6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5B47G9GG22N9KBNEG', 'T-156', '2026-05-23 11:15:08', '2026-05-23 12:05:19', NULL, '35180e547e94880bbe7c9f73b8b28e2c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4XM8EY708NAM1JK00', 'T-154', '2026-05-23 09:52:32', '2026-05-23 14:50:17', NULL, '45a2e199fca3eb06e286f98d5c8c2228', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5R9Q3T7CBMTHM2GFR', 'T-155', '2026-05-23 11:06:50', '2026-05-23 14:50:17', NULL, '578a38cb9982309cac40cd0a214d7839', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6CYT7NW4WE8ZP1TXR', 'T-148', '2026-05-23 07:03:18', '2026-05-23 15:05:29', NULL, 'edd29e766c59c65f2ca3c83700ed96f0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5V9RNW2BGT5GHRTRG', 'T-141', '2026-05-22 15:59:34', '2026-05-23 20:49:44', NULL, 'c95d953ec43d43dde07d94192342c3a9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QZ3JJF1704AV5JCW', 'T-157', '2026-05-23 20:48:31', '2026-05-23 20:57:07', NULL, '96ab8af2d5b2835a37c23b1af6c25be1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6KD2DKTGHC6CF1MJ4', 'T-159', '2026-05-24 08:53:13', '2026-05-24 08:54:30', NULL, '6d88f03a4c6268aefb7ab51411e6970f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7SPRJZ519RZK1ZPAW', 'T-161', '2026-05-24 09:24:02', '2026-05-24 09:45:07', NULL, 'e5ad416566fa75f3caf1ccf085aa2506', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5932T486Z1F1M6X6W', 'T-173', '2026-05-24 16:45:40', '2026-05-24 17:10:07', NULL, '0d564a0dc8adc327e3f9460c254499fa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AHSN3BXZFFWS0EPM', 'T-165', '2026-05-24 16:26:15', '2026-05-25 06:29:25', NULL, '879dcc70eadbc3d29a1e6f81ee0e86d4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7KA37YDBJ89GJ27H4', 'T-166', '2026-05-24 16:26:23', '2026-05-25 06:31:08', NULL, '6a6a0ba97d6331ac3441060ec28197db', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7K30Y9WAYZ3D330YR', 'T-175', '2026-05-25 07:11:09', '2026-05-25 07:25:02', NULL, '1d7a94fb62083c7c4390103f5bf29786', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6RP0Q312AZJ8S1Y6R', 'T-177', '2026-05-25 07:11:22', '2026-05-25 07:25:02', NULL, '59a180929bd8894b53a761b2dda7b4f1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7AXEWDKD4VQNQCACM', 'T-176', '2026-05-25 07:11:16', '2026-05-25 07:27:57', NULL, '231d303368c603a6e01a6da02802fbc4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4VNVEAEYBX3S9Z8Q8', 'T-178', '2026-05-25 08:10:10', '2026-05-25 08:21:46', NULL, '3f7aae66c9e487bef74fd4d93b304d12', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4DTGYPAB29MRDN000', 'T-179', '2026-05-25 08:10:18', '2026-05-25 08:26:21', NULL, '826c15b06fe0aaaf660e31042fe371bd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48P8T23ZPR0BRK6PR', 'T-169', '2026-05-24 16:26:49', '2026-05-25 12:12:29', NULL, '99d40776974ced8e57efaa135df3ea9f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM654XV2TG4K0RVV1CW', 'T-170', '2026-05-24 16:27:01', '2026-05-25 19:38:16', NULL, 'd4a19feb40528dd6d22186c442c46890', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4R8YY69YAARCVE4AC', 'T-160', '2026-05-24 09:13:42', '2026-05-30 11:05:56', NULL, '87a62aa99c4c3e3be11c031c5667ff68', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7G7MK8MP3BGQC36HG', 'T-167', '2026-05-24 16:26:30', '2026-05-30 11:05:56', NULL, 'a7343c529fc0c766f4d7c2a9530614fe', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78W48QX3JZP0QYAVW', 'T-168', '2026-05-24 16:26:39', '2026-05-30 11:44:28', NULL, '65bd7ae87632b0d65777a26af0bb31f2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6FMAHXM1XAHHCQC60', 'T-171', '2026-05-24 16:27:09', '2026-05-30 21:11:26', NULL, '556df973d835841f7e8ae0c8cd9dd310', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7ZB63YYNFXM9DQNRR', 'T-174', '2026-05-24 17:11:17', '2026-05-30 21:11:26', NULL, 'db290b95db28db6b5a90d880d8a36fdb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM71N3YMS82BAYD3M9R', 'T-172', '2026-05-24 16:27:16', '2026-05-30 21:34:56', NULL, '8fca1700a4bf028e071cf30e845ee8f1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6V6TKRPA106D5SMMM', 'T-180', '2026-05-25 09:15:58', '2026-05-31 08:49:07', NULL, '412af567b67756da2e6b438c6a4bfd45', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7WE0SD9N4HCP9BB64', 'T-183', '2026-05-25 09:26:40', '2026-05-31 09:30:11', NULL, 'f09d4e58d36e3049bbcf5d92df301530', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM55DZN2RFEVP6CG48G', 'T-184', '2026-05-30 11:42:53', '2026-05-31 10:17:09', NULL, '7f2e958614fa540d1087d3f1c84568df', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM52PRNJM86A1DZTQ1C', 'T-185', '2026-05-30 21:34:26', '2026-05-31 10:22:02', NULL, 'd7d14700bc8fb0164e6e2852b7a358cd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5HM3N07VB3RPFJCFM', 'T-164', '2026-05-24 16:26:05', '2026-05-31 10:22:03', NULL, '8bc4ad2dbf63176d9674cd014c6cce7b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4TDSA2PDCV814E3YW', 'T-95', '2026-05-08 10:48:59', '2026-05-31 11:20:21', NULL, '628386f31fcfb4e96eac7b503ab2eb4b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5316HVGDYD2W0DC50', 'T-162', '2026-05-24 09:29:07', '2026-05-31 14:17:22', NULL, '46d071266f94b404760832f3c2d88927', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM514YJBF8AY8Q4N7Q4', 'T-188', '2026-05-31 11:38:28', '2026-05-31 14:17:22', NULL, '7328143d32f30fc412c22b5d76de92a9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GVWHMRTMQYRTRKD4', 'T-187', '2026-05-31 11:38:20', '2026-05-31 14:17:22', NULL, 'ce42be5c4b71e4c155b26e22506045f4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4DAQ2CEFESG9JGMZ8', 'T-192', '2026-05-31 12:08:42', '2026-05-31 14:46:39', NULL, '6bd69db546e3d3aadb77b5c289db817a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6S7FDAXQX32JNCSFG', 'T-186', '2026-05-31 11:37:34', '2026-05-31 16:40:19', NULL, 'eed0473280a7c582289f0fcaa6bf6dd5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM709YPWECH1E66XXD4', 'T-193', '2026-05-31 16:12:42', '2026-05-31 17:26:25', NULL, 'ffbdde83a1cd6bbe9046f686705b2e15', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6YYG4X5R1GX97TJB4', 'T-52', '2026-04-23 20:32:06', '2026-05-31 18:54:02', NULL, '1e34fa00e15b0ada663e117c7493ca69', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QPGVP54EKJPCNQQM', 'T-51', '2026-04-23 20:32:06', '2026-05-31 18:54:02', NULL, 'cc6cf69d394f87a29c685b27ec13155e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM452XPSG99PA60ZZ48', 'T-53', '2026-04-23 20:32:06', '2026-05-31 19:41:13', NULL, '22e98aa4226d3e9979abd7b7cdf7b68a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
@@ -0,0 +1,263 @@
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7MFNNCC0MMY2TCNRM', 'T-194', '2026-06-01 06:49:25', '2026-06-01 06:49:47', NULL, 'c4ed6eb84383fb6052a61f94e37c98c9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4M2Z9YZ0SA81E5Q08', 'T-195', '2026-06-01 06:56:05', '2026-06-01 07:08:16', NULL, '6d199187494f2f942598092f9743285f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6NTYAB8K6T50F8W00', 'T-196', '2026-06-01 08:58:10', '2026-06-01 10:45:14', NULL, '57fadff50bee964e4d821507d0e6e79b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6HQXNNF05GXWK7BWM', 'T-198', '2026-06-01 11:11:32', '2026-06-01 11:31:39', NULL, 'f203d6089bcb5f23d81e4c3cf67b3d33', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM68APBCQTMGJAZ2B9C', 'T-199', '2026-06-01 11:38:32', '2026-06-01 12:35:04', NULL, '6fda1ce6b9f1ea55f001f1840fef1f7a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4RAKSX59W1HJCZ1M8', 'T-200', '2026-06-01 14:04:00', '2026-06-01 14:04:24', NULL, 'b2c666f841d2546d0e6f150dec9e901b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM57GY6PTG78SD3961M', 'T-201', '2026-06-01 14:25:59', '2026-06-01 14:45:39', NULL, '289f9f811f27bad8b3f01acda2651b4b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7G8QBJ2PNQ0VREN24', 'T-197', '2026-06-01 08:58:19', '2026-06-01 16:36:38', NULL, 'a29f2ef4fa8bc86eceff40c324d613d0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5A3984KFPD4NKRG6M', 'T-202', '2026-06-01 16:48:31', '2026-06-01 16:48:56', NULL, '726f989a61da913e9f41f3353e453541', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM643F7Z648KNFPT7GR', 'T-203', '2026-06-01 16:48:31', '2026-06-01 16:48:56', NULL, '736af2dbfdf6cfbc8d2683c8e0feb063', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM65R8CNFNE1MA5GGR0', 'T-204', '2026-06-01 18:48:19', '2026-06-01 18:51:08', NULL, '7685b5bd09a25bb7094a7458f292e81b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6M83TTXHRXESFZY14', 'T-207', '2026-06-01 18:48:47', '2026-06-01 18:59:19', NULL, '0fd6c0e9537080a0b8ab85f351161542', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4PH1J6ENYT4PJZRHG', 'T-205', '2026-06-01 18:48:27', '2026-06-01 19:12:01', NULL, 'e874c6bf6c97dae25404d6f1044f9c53', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5REFZXMY8ESWN0Z90', 'T-206', '2026-06-01 18:48:40', '2026-06-01 19:27:18', NULL, 'cdf5b34234372c3de0ef0c3caecabed2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4C2PSN2GVJY9JDSZG', 'T-210', '2026-06-02 18:13:52', '2026-06-03 09:00:37', NULL, '1281485b320f6c146f8af2519c0c02f3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM48QJV4N7J62436GC0', 'T-213', '2026-06-02 18:13:52', '2026-06-03 09:00:37', NULL, 'ac6cf78658e8a9ddfee7573e125377ed', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7171NRB1T82TGP5RG', 'T-211', '2026-06-02 18:13:52', '2026-06-03 09:00:37', NULL, 'bf74e45672e065ef2aa21ce85c341bb0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM54WTNQDMYT0AVR4WM', 'T-224', '2026-06-02 18:13:52', '2026-06-03 09:05:50', NULL, '16525f2ae7035719b319a6f579d5e316', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4TP6KXTB1GHKBW1G4', 'T-215', '2026-06-02 18:13:52', '2026-06-03 09:18:28', NULL, '4fcb599f4b489ac8050770416737295d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7WYF8ESBSGAQF1S48', 'T-217', '2026-06-02 18:13:52', '2026-06-03 09:18:28', NULL, '682a13f3d6e396600a5d51d38e9508d7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM51CNEJP52P79RDVJW', 'T-216', '2026-06-02 18:13:52', '2026-06-03 09:18:28', NULL, 'e89cf8d8a6c65f6c47ef73aa0f1a8d7d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78J2H2S0R79SSSPW8', 'T-214', '2026-06-02 18:13:52', '2026-06-03 09:18:38', NULL, '32ca757e5489e190af0dae4297b200d7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XZQJAK31WRVQ0HGW', 'T-228', '2026-06-03 09:49:35', '2026-06-03 10:02:02', NULL, 'ce3d4e957ec8cf656f8f022e518bf8d0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7SA9TEX5CNMTG67MW', 'T-163', '2026-05-24 09:42:44', '2026-06-03 10:05:55', NULL, 'abed11805979ce7f3d996679ad7007a4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5PFAYE4XH7WV6VDW8', 'T-227', '2026-06-03 09:48:23', '2026-06-03 10:09:26', NULL, '99368eee1c22ea3cf782b52159c04dc9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM68T660K07YH3X5DH0', 'T-229', '2026-06-03 09:51:28', '2026-06-03 10:09:26', NULL, '9d77e30754feb77e0a64a4aa5d47f668', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6Y5SET5WGSS2W78F8', 'T-219', '2026-06-02 18:13:52', '2026-06-03 10:31:10', NULL, 'fae2d0c2950c6f4050c70e7c3c07a525', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6EGSD0SB7NYNSP1BM', 'T-220', '2026-06-02 18:13:52', '2026-06-03 10:59:25', NULL, '3c812d20f5da80c0b936cb59edd2704f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67ET3VQP90WRXVSMG', 'T-181', '2026-05-25 09:16:05', '2026-06-03 11:41:17', NULL, '045b971616418029b63a850050eef8a5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM758PXMJH16PA3JHFW', 'T-221', '2026-06-02 18:13:52', '2026-06-03 11:50:02', NULL, '9e906306ef56b2c22724d0b72f14ce2d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7AMEYEBTF8YGJ4BVG', 'T-218', '2026-06-02 18:13:52', '2026-06-03 11:50:08', NULL, '9c2a9480a37df7681fe4388e27345573', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GJRWZVHZF24TPPS4', 'T-226', '2026-06-03 09:37:54', '2026-06-03 11:50:23', NULL, '6bf4765285f29d4c8fce8d71472bae0c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM427BVZYN6MD7ZZX74', 'T-231', '2026-06-03 13:17:32', '2026-06-03 13:30:05', NULL, 'd6eb4f7458f0d1c72432b87a23aa60e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5BJCK5YG3R0NEEF70', 'T-232', '2026-06-03 13:29:49', '2026-06-03 13:41:42', NULL, 'f255d56c3872bf6a210734e127a4a4bd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JR6QZDMKE167C4MR', 'T-82', '2026-05-06 08:30:15', '2026-06-03 14:56:07', NULL, 'd2669b112125f4f76041885296f5d315', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM492DTRZJK9CGQPGDC', 'T-234', '2026-06-03 13:49:44', '2026-06-03 15:04:07', NULL, '92a7cabc855b76f429217cc65fcfa308', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5GH6VP8TZA95NMYK0', 'T-230', '2026-06-03 11:15:39', '2026-06-03 15:17:33', NULL, 'eec8da81c7180e8f301196514ba54e3b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM46ANXPW02XGD3D3NG', 'T-237', '2026-06-03 15:43:45', '2026-06-03 21:29:47', NULL, 'f9b1b279c9292d892ac07b7f4ea7cef8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4W624VD4JW1GHDHAR', 'T-182', '2026-05-25 09:26:31', '2026-06-04 12:00:09', NULL, '35ccb60111678fbc43644460a31f1d61', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM56FKPV4WSPWA0KMB4', 'T-189', '2026-05-31 11:38:36', '2026-06-04 12:00:09', NULL, '3759a2e27d13bddbcc19ed9a4fffef9a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5V7GHH42RHV04DPTR', 'T-65', '2026-04-24 06:34:16', '2026-06-04 12:00:09', NULL, '544a078f04d5c926a47cbfdacb4e3e2f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5BWXW61EVYGMZD47G', 'T-191', '2026-05-31 11:58:19', '2026-06-04 12:00:09', NULL, 'd186513cc0783306b2c4e1b2e789dff7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5ABRPK2SPW8VY7RP4', 'T-190', '2026-05-31 11:38:42', '2026-06-04 12:00:09', NULL, 'fac1c8383ac95413e8885f8ee64b0d54', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM41S6RRTMTY9TYZA50', 'T-122', '2026-05-18 09:06:59', '2026-06-05 13:08:05', NULL, '600101f09173e61159a24ed44e34dd71', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66TK8TQ1X5T9GK6V0', 'T-42', '2026-04-22 21:03:40', '2026-06-05 15:17:09', NULL, '223a9820685b3da2a6a6e19cf6ad036b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5WT3BHB8JZY77G5AW', 'T-243', '2026-06-05 15:14:25', '2026-06-05 15:17:09', NULL, 'd82988ff85cd644376a13f49e406138f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KPN4VK4EZP013NNG', 'T-244', '2026-06-05 15:21:10', '2026-06-05 15:24:35', NULL, 'f375c96bfd96b9cdd89db0cd046318f3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYY4FVPX9KRVHCMC', 'T-50', '2026-04-23 20:31:48', '2026-06-05 21:12:23', NULL, '581605b67cbab29fdcddbac46154308a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM66DNEK5FS9C0T4J5R', 'T-245', '2026-06-05 21:20:00', '2026-06-05 21:24:52', NULL, '577902ab47810d0f848f48c8bf9b3196', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6DH7C6GQN9WNKQNXR', 'T-246', '2026-06-05 21:28:44', '2026-06-05 21:29:50', NULL, '4ce06c5b0b8a09d452934ae8ba1e5c0b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6QCYT7JBF352YVJ48', 'T-247', '2026-06-05 21:28:48', '2026-06-05 21:30:04', NULL, 'c2fbcdc7eda67938f8d35f64a92782b4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F4D2Q363QF7V40K8', 'T-239', '2026-06-03 21:41:58', '2026-06-06 06:44:40', NULL, '7312cd1c56d3c8d60625df7712a7deb4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5F2GC0ABFFVAH34EC', 'T-238', '2026-06-03 21:11:40', '2026-06-06 07:24:00', NULL, 'a724281049b28271d21c87776313dd72', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7ZQNDVPAPFSD9KYS4', 'T-251', '2026-06-06 07:40:43', '2026-06-06 07:40:43', NULL, '9f010efd2f1b44840ff0af4d3723a4af', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BVE5EYM92CWGSXA4', 'T-250', '2026-06-06 07:26:50', '2026-06-06 07:49:08', NULL, 'a8c1c3f7d308c4fe005fb998b7ec6730', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7JXD3FKW91934D0W0', 'T-249', '2026-06-06 07:21:17', '2026-06-06 08:21:40', NULL, '9f43580f953cac1a27dab6e0ac2deece', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM70TW38E78FFXDHYAC', 'T-248', '2026-06-06 07:21:17', '2026-06-06 08:31:25', NULL, '577fda356a95641dd39c28a716227d1a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5NEXC0P46NJHACNM0', 'T-233', '2026-06-03 13:29:59', '2026-06-06 09:20:02', NULL, '4cea7def08ae19b77ecfdd04b11f4b5a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM78Q3VYWSE830CTVB4', 'T-212', '2026-06-02 18:13:52', '2026-06-06 10:02:03', NULL, '75bc38afe04d24ff77c2b55d9b764fe1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QYYKHPQ03H3QD61W', 'T-209', '2026-06-02 18:13:52', '2026-06-06 10:02:06', NULL, '4f782ec2f1fb76eea268247cf4cd75a0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4K2AAPK52Y8BEJ3YR', 'T-257', '2026-06-06 14:01:03', '2026-06-06 18:58:25', NULL, 'd79886881baf9d88ff31ec2970da2712', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5KYAV8TMH3Y3FPRSR', 'T-23', '2026-04-22 14:08:40', '2026-06-06 19:06:30', NULL, '9b456adde02b184d5d46652aac210af9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72ACQSDBJTKRBC58W', 'T-223', '2026-06-02 18:13:52', '2026-06-06 19:24:44', NULL, '7909a9620aa2a05d6a02b36f030f3641', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM620F5ENRYNT9S0JDW', 'T-222', '2026-06-02 18:13:52', '2026-06-06 19:24:44', NULL, 'b58b0643ed78d32df526e765a076b2eb', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5DYC7CRP8TMHADW8W', 'T-225', '2026-06-02 18:13:52', '2026-06-06 19:37:47', NULL, '6d2670282a9f66635c2fccaa20162209', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XMQ44AVCAARQZX14', 'T-208', '2026-06-02 18:13:52', '2026-06-06 19:53:07', NULL, '015f7fe6ec6e3b348a6157401742472b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6S7KHYPE9W7S1JHZ8', 'T-256', '2026-06-06 10:07:35', '2026-06-06 19:53:07', NULL, '2dbd4405f72911a87fd757c819b9f0c5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6WZ7DKVDFAGQ4QDMG', 'T-258', '2026-06-06 20:55:49', '2026-06-06 20:55:49', NULL, '4bcfdcf889c4bf6ca3405fa7b4857520', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7RQMN1GKEZ5JQKJT0', 'T-259', '2026-06-06 21:12:44', '2026-06-06 21:12:44', NULL, '9efbdb558a1358b67827a0814fa5c23e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5JK0RHMRAH47K8ND4', 'T-36', '2026-04-22 20:34:09', '2026-06-06 21:14:22', NULL, 'bab3e0865388263d61db6651c3e5a55d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6DSPH569T4458B8JC', 'T-260', '2026-06-06 21:46:42', '2026-06-06 21:46:42', NULL, 'e45747cbef04e469b21ef0b4f2a3122f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7GJW4V3CXMMXXV988', 'T-261', '2026-06-07 08:03:10', '2026-06-07 08:03:10', NULL, '54c9a91b6b4408f05c85ab0afb7daf47', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7GCVGCH9RMGYHENCC', 'T-54', '2026-04-23 20:32:06', '2026-06-07 08:03:17', NULL, 'ed036c636484f9b724fe82dace896d49', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4QW8VH454J8V3E174', 'T-252', '2026-06-06 08:28:43', '2026-06-07 08:32:36', NULL, 'cb647a08e949e13f6386fefbb99c8c6e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63WP564EZRERJHVCM', 'T-255', '2026-06-06 09:56:23', '2026-06-07 08:51:08', NULL, 'e2aa43fb1e6a1c9c28ebd1fe2ab6bd79', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM74JAV0MJMB4VQNTRG', 'T-268', '2026-06-07 09:35:03', '2026-06-07 09:36:59', NULL, '796bd5eff55061d92fe00b3c4b15d3ad', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6P9YQ5V7WS6RFJNHW', 'T-269', '2026-06-07 09:44:17', '2026-06-07 09:51:32', NULL, '6fc792db7c0d2ba8f8c73c1e1960faf6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4XBRZJ6DEHFWREWC0', 'T-271', '2026-06-07 10:42:32', '2026-06-07 10:42:32', NULL, 'a7a8244eab996f86026f60edd5969de6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM67PDXAFTPPW7B1WDR', 'T-272', '2026-06-07 10:42:38', '2026-06-07 10:42:38', NULL, 'd3fcdcc97810a9b5f6b0624a3507c952', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM40JE9MZZWCXC91THG', 'T-270', '2026-06-07 10:23:13', '2026-06-07 10:43:18', NULL, '014c8442d8f19f6cc075ad8fc27ce9ed', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM551VZ2NYV6PD0XQRC', 'T-48', '2026-04-23 20:30:09', '2026-06-07 15:40:06', NULL, '8389d1a6a5d2db863bda0f7405d9496a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM71P14PAF7K4QE61G0', 'T-262', '2026-06-07 08:10:42', '2026-06-07 17:41:11', NULL, '561c4e90d842e6ac1a22dcacd7a5e33f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5W6VN98RQM6S22X28', 'T-274', '2026-06-08 07:26:13', '2026-06-08 07:46:46', NULL, 'd9177bbb472fd16a13d4f23e0cd70344', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5TWC00GW0P3X02HZW', 'T-276', '2026-06-08 07:46:23', '2026-06-08 07:53:32', NULL, '1b77c5f3a8d66247532ead2164b6b37f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM60PAS5RJ55CZGPM1G', 'T-277', '2026-06-08 08:32:30', '2026-06-08 08:34:47', NULL, '70e24eabce743397720db9d6029a3337', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7XXSERDHKR0EEH3N4', 'T-263', '2026-06-07 08:34:02', '2026-06-08 08:45:23', NULL, '1cb588e500a902d2b880f3004c5c33b4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45HHRBCRMV2166Y6M', 'T-266', '2026-06-07 08:41:13', '2026-06-08 09:04:12', NULL, '9162b1edab8984a25660f0fdcebcd41d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YYMMKFD5Z8WG9F60', 'T-264', '2026-06-07 08:40:25', '2026-06-08 09:25:50', NULL, '4b9c66ff7114ce85456f57f6d1c58d7b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4Q1Q97CHA0XNK2G4W', 'T-265', '2026-06-07 08:40:33', '2026-06-08 10:09:14', NULL, 'ff24fcb74089501588a4bfcfa62c9aad', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7ZGZ9GQ5W1MKV00Y4', 'T-267', '2026-06-07 08:42:53', '2026-06-08 10:09:21', NULL, 'd860955c7a7030d4c1c2ea98e5447fdf', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM55CV3VDYS88VBJZ60', 'T-273', '2026-06-07 17:39:10', '2026-06-08 11:44:27', NULL, '02cd1eaf484bd3adef09a71a7408674e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM59DG2WADJ9A0JFD1W', 'T-281', '2026-06-08 11:46:11', '2026-06-08 11:49:36', NULL, '1d9df18227de229f12bda189c4081353', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6K1QRC911JMQ9C960', 'T-283', '2026-06-08 13:39:51', '2026-06-08 13:44:57', NULL, '56929fab7dc064046853959720d43b8b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6A6A56VFSVRRBKMP0', 'T-279', '2026-06-08 10:21:20', '2026-06-08 14:12:46', NULL, '1023e2106149f0366bf3896635ec44c4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KFHAS1WZFGBJ6JN4', 'T-284', '2026-06-08 14:55:43', '2026-06-08 14:57:15', NULL, '75d5f69fd00e63d09e707ad1e8338853', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6GZ55VD4SAZZ6XXP4', 'T-285', '2026-06-08 15:22:32', '2026-06-08 15:37:29', NULL, '994bf6438e27af887c2f3d247e76d576', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W2MWZ45YNK7TJGC4', 'T-282', '2026-06-08 13:39:15', '2026-06-08 15:42:01', NULL, '9d5f153703ebeae7f011f555f380ef6e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5AAA9WW54JGP83HGW', 'T-275', '2026-06-08 07:28:34', '2026-06-08 16:52:13', NULL, '886bd74d7ec01145f8acb5c7d9397307', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6NNRR61E8P6GHXABG', 'T-289', '2026-06-08 20:16:22', '2026-06-08 20:16:22', NULL, '6c2022166a2139a665912c3fe6d4802e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4501EBPDFDH84RJ5G', 'T-288', '2026-06-08 17:52:51', '2026-06-09 06:19:01', NULL, '16b1c85fab4273ebe7c9cfb9b9800e3e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5R93N5J6RNVTBRB28', 'T-286', '2026-06-08 16:30:27', '2026-06-09 06:19:36', NULL, '22c1b0f40e3e3344f0c624af618bdaff', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM44CKN5QQGFKP1KWZC', 'T-290', '2026-06-09 07:15:14', '2026-06-09 07:15:14', NULL, '53ca5591666fa7c01d90174bf28ecf4b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM45BSF1B0R0JA1Z7FC', 'T-291', '2026-06-09 07:15:22', '2026-06-09 07:15:22', NULL, '79e9f769c9313f0bc63d5c8c8a1ee641', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5E41Q8TX8X55X4MZ0', 'T-29', '2026-04-22 20:17:26', '2026-06-09 10:46:41', NULL, '93ab2ce409ff22e37a756e41553c0a69', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50BPNDT50PM24ZVA4', 'T-278', '2026-06-08 09:20:00', '2026-06-09 15:03:23', NULL, '0c340b4aeb41ddaa858ae18888207b2e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BD8PXDRHBF001200', 'T-292', '2026-06-09 14:57:32', '2026-06-09 15:09:05', NULL, 'b907503e714ddc775257c90330c25d2f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5DM43Q9HX352STN3M', 'T-295', '2026-06-09 15:26:00', '2026-06-09 15:26:00', NULL, 'c93802bd37eb8dfe981c74cfde3f24c4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42WW0SY3XEXKE8PK8', 'T-293', '2026-06-09 15:12:15', '2026-06-09 15:36:15', NULL, '55da76c0ebfc56f44cb4096a2a17b9c6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4GN07FN8GAXZ20YAR', 'T-236', '2026-06-03 15:31:01', '2026-06-09 15:55:06', NULL, '18d3d9bc2bc268d007d27c4cc20fcea8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM61SRVBDRFFN2S80T4', 'T-254', '2026-06-06 09:32:54', '2026-06-09 15:55:06', NULL, 'ae31cf118f94e93699eed5f0ac0eee7c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM63TRGBFRWK9W2WA98', 'T-296', '2026-06-09 15:43:58', '2026-06-09 16:34:46', NULL, '405de86b123fc1aeccf6f444a36bbf36', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM72SHY93S36VM9G6D8', 'T-294', '2026-06-09 15:18:22', '2026-06-09 16:34:46', NULL, 'baa483c978c2744aaf95035de3809048', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6RXN6HW6XTEHKYQDG', 'T-297', '2026-06-09 16:01:09', '2026-06-09 16:34:46', NULL, 'f228757d5b5ed41b7cf90b6035e09dda', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7K1FN7KD8S5ZVX2VM', 'T-299', '2026-06-09 16:42:47', '2026-06-09 16:42:47', NULL, 'a2fdfc4298353c34568d42e71c2786e6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM53HQD6YV754GKBA60', 'T-44', '2026-04-23 20:25:26', '2026-06-09 16:56:55', NULL, '75a46ae8c87aa5a9a27e513b4ea65e58', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM62FKQQD0B9B80PFY4', 'T-158', '2026-05-23 20:48:38', '2026-06-09 17:16:34', NULL, '1282d8b5cb009f639f7c7a14e69ef2b3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7D5DMYF5X92Q55CBW', 'T-235', '2026-06-03 15:17:29', '2026-06-09 17:16:34', NULL, '1bccb4cf47ba2a1a72e4f65a214e2f78', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7W0NPAEG3KWNFY9DR', 'T-132', '2026-05-22 15:45:15', '2026-06-09 17:23:25', NULL, 'cf1709ddff1d13a7b4adb0dc193b319b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7F2TBJJV1F2KP7XR8', 'T-300', '2026-06-09 20:02:41', '2026-06-09 20:03:08', NULL, '6f1c204fc4f81403ba50e9e7ecff7241', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6048VEGFDNEBHFMM4', 'T-301', '2026-06-09 20:04:29', '2026-06-09 20:04:54', NULL, '89490f3e4d3e09714b70f0e35c726038', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4AE2GEF6BXSGKRMGC', 'T-242', '2026-06-05 10:17:47', '2026-06-09 20:07:30', NULL, 'c570cc4f2808f31192d947affb3392a0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM42VPM50VKAWXBCAM8', 'T-253', '2026-06-06 09:32:53', '2026-06-09 20:17:04', NULL, '47b9d1a167b65553a14ede7a5a53f851', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM7H8JWCP59WQVD0FW4', 'T-302', '2026-06-09 20:24:54', '2026-06-09 20:25:49', NULL, '8e04d93f6163a8f54965bc5988dae31c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5QJC039TMDF5JGMXC', 'T-240', '2026-06-05 09:38:46', '2026-06-09 20:27:17', NULL, '4879c042965effced12e608af5739659', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM531VE0C6N36AT12SR', 'T-241', '2026-06-05 10:02:31', '2026-06-09 20:34:30', NULL, 'b7de744d4e5292306b1fa64bcc3ba1aa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4K1MTP2MHRKKNXBFM', 'T-303', '2026-06-09 20:41:22', '2026-06-09 20:41:22', NULL, '1adc6db2fcc64d6fd36ad0406939a8d7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4BRWWPB4R06Q31XV0', 'T-56', '2026-04-23 20:32:06', '2026-06-09 20:57:53', NULL, '5a394b49d98e119d21b637745798fd8b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM6SK3D1QYACBPHHBZ0', 'T-287', '2026-06-08 17:12:49', '2026-06-09 21:21:08', NULL, '7d9537b53ade941ecd8a8efadd591c17', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM4KS233FGZE9H7ABWR', 'T-304', '2026-06-09 21:22:28', '2026-06-09 21:22:44', NULL, '252c572a5954883d0e5e71406b092a7f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM50S6WRD81V3Z0NH1M', 'T-298', '2026-06-09 16:06:41', '2026-06-09 21:36:05', NULL, 'f8666523eaf2642b918f6846e2801539', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB0TNQM5YNEQM1WY25BV7TD8', 'T-280', '2026-06-08 11:29:22', '2026-06-09 21:42:59', NULL, 'e9859dd425e0ac667ddbce721415572e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16FJ7KXGFHQXEG88MYRFTG', 'T-305', '2026-06-10 08:15:39', '2026-06-10 08:15:39', NULL, '288b0c9d74e98c2ac42ee25d3369f689', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB16YM1Y910T07Y1Y22MGTEM', 'T-306', '2026-06-10 08:17:42', '2026-06-10 08:17:42', NULL, 'af36af77224b697a61cb5447955605e5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB170DJA02EH2E8HMH7X7SS4', 'T-307', '2026-06-10 08:17:57', '2026-06-10 08:17:57', NULL, '882cca1753c2ede7bfe2aef142545b76', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1Q1Y3CYJHD7W5F68VDB6T4', 'T-308', '2026-06-10 09:28:04', '2026-06-10 09:28:04', NULL, '5050ff0e69b24a93f5b7e96c8a9fa47e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1S7613SYF0M9XQT5JNWM40', 'T-309', '2026-06-10 09:37:31', '2026-06-10 09:37:31', NULL, 'b4f48310ea6b0f49f83c46e3bc5b7ad8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1SJKA7KRCQMZE41SPQG780', 'T-310', '2026-06-10 09:39:05', '2026-06-10 09:39:05', NULL, '9088122bf9d5e04ca8a7fee3bf031a53', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1XDWKQ594ET4GDYEFK5ZJ4', 'T-311', '2026-06-10 09:55:55', '2026-06-10 09:55:55', NULL, 'ed03464125d09ff5f23913e6247064e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB1ZFJK6J2GSV4SA69QF730C', 'T-312', '2026-06-10 10:04:53', '2026-06-10 10:04:53', NULL, 'dd22af12b333c13376b23aa5381ea2d7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB234WP4Y6Q16A0HFW8BSXMG', 'T-313', '2026-06-10 10:20:54', '2026-06-10 10:20:54', NULL, '9b7abb73ea56cfdad2039303a8935897', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB23Z52EVGQ4ZDAQ5BVAJ13R', 'T-314', '2026-06-10 10:24:29', '2026-06-10 10:24:29', NULL, 'f8b89421a13f620c2a0e76bd780db5a2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ACSDBDZARV3NNGYD9NYYR', 'T-315', '2026-06-10 10:52:34', '2026-06-10 10:52:34', NULL, 'd0521ab80265d66c99c803a8714e8f02', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2AD3HPR3HXSVASVZEX8PK0', 'T-316', '2026-06-10 10:52:36', '2026-06-10 10:52:36', NULL, '5beb005b263eb4c5bb10ffc6e2fc0100', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2EDCBYRBDSV9V1PJ1KE3CM', 'T-317', '2026-06-10 11:10:07', '2026-06-10 11:10:07', NULL, '4d4a02ab95987e3ebb134a2f0173b4a1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ERREMEEF26KKHGNZBWW64', 'T-318', '2026-06-10 11:11:40', '2026-06-10 11:11:40', NULL, '31820aadd1f19f9dd0cf931dae565abc', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ESCR4V6V07CRH18FBCDN8', 'T-319', '2026-06-10 11:11:45', '2026-06-10 11:11:45', NULL, '12c5a57fb83444410cca98a802e9ba6f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2ETJQP0CT6X7W3CWZ6NS9G', 'T-320', '2026-06-10 11:11:55', '2026-06-10 11:11:55', NULL, '2266e362f144e4fd97403e1d1abe3eb4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2EV29HSK6EJ5VF50R87VC4', 'T-321', '2026-06-10 11:11:59', '2026-06-10 11:11:59', NULL, 'd48b6bd9f0c8289c95448dda6865d666', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G1WD1839Z90AQ5C0BHNV4', 'T-322', '2026-06-10 11:17:17', '2026-06-10 11:17:17', NULL, 'f33b1db4b67a0521f02c5c49fcfa8e6f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2G2KHKT5CJYR0TK1WQGMD0', 'T-323', '2026-06-10 11:17:23', '2026-06-10 11:17:23', NULL, '0738a8da475a6d8d49132d1e4e753775', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2J2HWD66QAFDDRRWS5NM48', 'T-324', '2026-06-10 11:26:07', '2026-06-10 11:26:07', NULL, 'd8aa9ae4522d97b70331c42b8ab7a365', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2T11GCV1EV07DYD5BZENTM', 'T-325', '2026-06-10 12:00:52', '2026-06-10 12:00:52', NULL, '47b8f31d9337c5bbc6476d62c7f6ed4d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2TY91VHK7TPKPMZ11EG3TM', 'T-326', '2026-06-10 12:04:51', '2026-06-10 12:04:51', NULL, '348dee79d9d3fe0a8b260c1f9c47a289', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB2W4G9K8ZF782W7H2TM5XA8', 'T-327', '2026-06-10 12:10:04', '2026-06-10 12:10:04', NULL, '967aab6bc692754a5d3a192cb0008871', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB37JZSFZKWPK9PDFYJY2YC0', 'T-328', '2026-06-10 13:00:06', '2026-06-10 13:00:06', NULL, '553b6683bbcddfbbab39ee545a99c98c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DHCTP001YCHFP39XER0ZM', 'T-329', '2026-06-10 13:26:06', '2026-06-10 13:26:06', NULL, 'a7b63cf73534de82c953d3029251b5e4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DJZDDZ00BSA04B660RS7M', 'T-330', '2026-06-10 13:26:19', '2026-06-10 13:26:19', NULL, 'c7ef552dc6d7fc2a5e312b20939a2987', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DKQQJ583944DG8561VQ3G', 'T-331', '2026-06-10 13:26:25', '2026-06-10 13:26:25', NULL, '002e4d81284119aecab05d180a9a41cd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DMF20SYFDT6WX2RFBQXKW', 'T-332', '2026-06-10 13:26:31', '2026-06-10 13:26:31', NULL, '38d661bbd4a937daf395f82074cf8d12', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DN94MBCTYJW17ZCYVSXE0', 'T-333', '2026-06-10 13:26:38', '2026-06-10 13:26:38', NULL, 'c710c528662f92c6b7416b7163c8e99f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DNQZKV20F7YG5PJH8V8SM', 'T-334', '2026-06-10 13:26:42', '2026-06-10 13:26:42', NULL, '7f774dc51ae344eddfd42c2149887d56', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DP48FS33CQGRDF7EB9GT0', 'T-335', '2026-06-10 13:26:45', '2026-06-10 13:26:45', NULL, '1cce32ce77997202bf74c69426c29ab9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DQEMTDHF8SV27AKAB8JHW', 'T-336', '2026-06-10 13:26:56', '2026-06-10 13:26:56', NULL, 'b988abcd2cef0dbd6adbc56502f676ca', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3DWCJSGZH9WYDNFWZBAYYR', 'T-337', '2026-06-10 13:27:36', '2026-06-10 13:27:36', NULL, '94b0ebd244a6794e0e4e5a868b38d67f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JAXDKZMS0805MMEYB6820', 'T-338', '2026-06-10 13:47:04', '2026-06-10 13:47:04', NULL, 'be9b536d471a6008cc854092dfa6aaa4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3JM0AXK1CTWD720RV1AVZ0', 'T-339', '2026-06-10 13:48:18', '2026-06-10 13:48:18', NULL, 'e76a2ce5d3149bb7ca0ec8a45f5c57a2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB3KS499THAD899M0NWN7E3R', 'T-340', '2026-06-10 13:53:23', '2026-06-10 13:53:23', NULL, 'bdd164a73576f6ca5151c8a62640526f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB44SKPKTHFMV6WD28GZYPXM', 'T-341', '2026-06-10 15:07:43', '2026-06-10 15:07:43', NULL, '113520f8f3a640fa2f470200ea9f45d8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB493JEW32CH0H3771TNHF7G', 'T-342', '2026-06-10 15:26:33', '2026-06-10 15:26:33', NULL, '13a646741953f113356c4bcda162c3c9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4FDREHRYRR7B9ER72KCQKC', 'T-343', '2026-06-10 15:54:09', '2026-06-10 15:54:09', NULL, 'e48d2b913bb03202409c6568d9bc11a5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4J5E6W983P1S7BE0FDPSMM', 'T-344', '2026-06-10 16:06:08', '2026-06-10 16:06:08', NULL, '481e82dfab98c20cd37c77a3e0c16668', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4RD1DDSYM4J7WYEGTXARB4', 'T-345', '2026-06-10 16:33:23', '2026-06-10 16:33:23', NULL, '9f26d5036c05057d4bd0fc53ddaddafd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4VG3N3YJSV8G7M1HFSYW2W', 'T-346', '2026-06-10 16:46:54', '2026-06-10 16:46:54', NULL, 'a64940cb0b4ec4fb489eac27065dc447', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB4XCM5KBXDDSCWJ37GPYG3R', 'T-347', '2026-06-10 16:55:10', '2026-06-10 16:55:10', NULL, '9d40226dbc6136072d2d5d0eda71f141', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB50YE6S6YWNP2ZSFWES9B2W', 'T-348', '2026-06-10 17:10:42', '2026-06-10 17:10:42', NULL, '8bb5ad92551f272417840869a0774668', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB58X0TFJ02YTMVPD0D9Q838', 'T-349', '2026-06-10 17:45:28', '2026-06-10 17:45:28', NULL, '0b12f14fa83254ab113c870531628359', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5CW7JPT6BR2RWMNYVCXJ50', 'T-350', '2026-06-10 18:02:50', '2026-06-10 18:02:50', NULL, 'a5c0c22d84621b14a5208317414d6026', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5HMYDXP62RKH3HP55T6AYG', 'T-351', '2026-06-10 18:23:41', '2026-06-10 18:23:41', NULL, '9d2da44c16c5aa38c0a36e4b00ef5f15', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FB5M14B76B31654D959XM5AC', 'T-352', '2026-06-10 18:34:05', '2026-06-10 18:34:05', NULL, '3fe3e1d5fb7c0fbd084b45116575ad98', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBAWHM1SQ1686ZJ8JQCFQ1ZW', 'T-353', '2026-06-11 06:50:21', '2026-06-11 06:50:21', NULL, '53374633101d04f94981baaf4f2e0315', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBDSJYQFDNKP4KA1JAEDSS8W', 'T-354', '2026-06-11 13:36:51', '2026-06-11 13:36:51', NULL, '32bb5d359401599022a8771031d8a08a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBDSKGAHYHH2NPZK8B6EV4D4', 'T-355', '2026-06-11 13:36:55', '2026-06-11 13:36:55', NULL, '2eb6809e958613a924b35e080ab17609', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBDSM0PRGYR61R0NWYAT9VDC', 'T-356', '2026-06-11 13:37:00', '2026-06-11 13:37:00', NULL, 'bdb597080218a3e8783f6c5cf74c529a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBDSPECQ0FPKB9SYTD7KZSBM', 'T-357', '2026-06-11 13:37:19', '2026-06-11 13:37:19', NULL, '96cf88e036dc3a45487cdbffff26cdde', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBDSQ2GBSP0ZH4RHZG2PMR0R', 'T-358', '2026-06-11 13:37:25', '2026-06-11 13:37:25', NULL, '2a656aaec54bf50c34c7e28347b1fb29', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBGHNEQTAEPGNJKN42C1E8', 'T-359', '2026-06-11 21:54:36', '2026-06-11 21:54:36', NULL, '1930fad7b18c79cf97d913d8372b77bd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBJ5T7HAQ9CA8XQMX43A2C', 'T-360', '2026-06-11 21:54:49', '2026-06-11 21:54:49', NULL, '1ffef3c00cdca28566ab67a11ec17e53', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBKK2TZQK683J8FS0ZH5A4', 'T-361', '2026-06-11 21:55:01', '2026-06-11 21:55:01', NULL, '5f8bdede98496496bf031a40a09dee16', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBN5F0F8SDF15P21DNKT1W', 'T-362', '2026-06-11 21:55:13', '2026-06-11 21:55:13', NULL, '610e74eb7b4ff9db3949ed01a0268380', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBPQE4J4YBJX92812ZK6DR', 'T-363', '2026-06-11 21:55:26', '2026-06-11 21:55:26', NULL, '5daf7bcd9ccdc37f8aed531ef12d395f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBR4636GSRJBWFJDAZ6ZA0', 'T-364', '2026-06-11 21:55:38', '2026-06-11 21:55:38', NULL, '025de46ba9e8d005fd8b1f74687cd8b6', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBSG6356MZJ2DCCCSBMBGM', 'T-365', '2026-06-11 21:55:49', '2026-06-11 21:55:49', NULL, 'b366b41e4737a2761a01284fd7dd44e0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBV0465906BY3QFAY9F1YM', 'T-366', '2026-06-11 21:56:01', '2026-06-11 21:56:01', NULL, '8a75dc7dea83a0e643d1912bda46dd57', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBWE2W1226T58CX37E50HC', 'T-367', '2026-06-11 21:56:13', '2026-06-11 21:56:13', NULL, '578b51eafd19044dc0e2720f6e55d633', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHBYTJ4E7ZBY6DWWNT1S16M', 'T-368', '2026-06-11 21:56:33', '2026-06-11 21:56:33', NULL, '0df409c83fe28af2d49a156118ed6ece', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC0HYRZ86CWW0DDQJ5CAQM', 'T-369', '2026-06-11 21:56:47', '2026-06-11 21:56:47', NULL, '7f47ddc3e84f9fea915200992a5a0baf', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC2NM7AYKENZ0ZD49HAX1W', 'T-370', '2026-06-11 21:57:04', '2026-06-11 21:57:04', NULL, '1908b7c0903c389ad5b743fb89ca32e0', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC46SEHH8NQY481VMGK66R', 'T-371', '2026-06-11 21:57:17', '2026-06-11 21:57:17', NULL, '2095f10159561a5e81b2a9b990c79fa2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC5ZE4EZEGXK8YY8J86CM0', 'T-372', '2026-06-11 21:57:31', '2026-06-11 21:57:31', NULL, '9d22f370e726af56d47d6c8acd93bc87', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC7KDFW07S8WTCC3MD71J0', 'T-373', '2026-06-11 21:57:44', '2026-06-11 21:57:44', NULL, '231c399183a83e569c0645e1d149625f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHC90B72A270CAKA7AP1ZX8', 'T-374', '2026-06-11 21:57:56', '2026-06-11 21:57:56', NULL, '9832ac0b3e0bebd85f3271a5ea96d4ed', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCAFKK334YNJXZJQG4J6AW', 'T-375', '2026-06-11 21:58:08', '2026-06-11 21:58:08', NULL, 'de3569d098b5c4cf6a3d49ee3d33d1c9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCC6AR37VTF4SY8DR99JHC', 'T-376', '2026-06-11 21:58:22', '2026-06-11 21:58:22', NULL, 'fe23c94d8971d21963a2b2e2739e05a3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCEC25337J2AXXQNST56Y4', 'T-377', '2026-06-11 21:58:40', '2026-06-11 21:58:40', NULL, '1585df7f3826ddbcef8a030ffd1e0890', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCG84F4SPFW111CC4K26A8', 'T-378', '2026-06-11 21:58:55', '2026-06-11 21:58:55', NULL, 'c6b0d9d124401f1d2bdc403567cbfdf8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCJEYHC91PMVNVWVHBR2RG', 'T-379', '2026-06-11 21:59:13', '2026-06-11 21:59:13', NULL, '0d9aed402c9840ef6fb75edfcfcbc3f4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCM1RBAF72SCZBKRTXJSYC', 'T-380', '2026-06-11 21:59:26', '2026-06-11 21:59:26', NULL, '1a0d767f30c9bc0e9c96f39d468ca67b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCP03EJ9CDBGZGRPD19N8W', 'T-381', '2026-06-11 21:59:42', '2026-06-11 21:59:42', NULL, 'cfac236e079164f9588d936f5101c71c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCQHZQ0NKY1VRWWPSNZT84', 'T-382', '2026-06-11 21:59:55', '2026-06-11 21:59:55', NULL, 'b54be44ec4cfbbc649324471a5e2141f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCST6CQ449VJGAP6C5KZ5W', 'T-383', '2026-06-11 22:00:14', '2026-06-11 22:00:14', NULL, '0e18de4e94e4fcf36fc40de763522cbd', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCVPGCKEGDC54KKQ120SRM', 'T-384', '2026-06-11 22:00:29', '2026-06-11 22:00:29', NULL, '57d7b6a6c05791acadd1cebe611c8991', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHCXFF5V1RT6QJETS2K4C0G', 'T-385', '2026-06-11 22:00:44', '2026-06-11 22:00:44', NULL, 'ebda2dd9c7dfe92bab2260dc34212ac4', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD098CV2N73823KX4Z99P4', 'T-386', '2026-06-11 22:01:07', '2026-06-11 22:01:07', NULL, 'de90501f2d4be80231651d25d04f4649', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD2FWTDFYA00W4QXTE41M0', 'T-387', '2026-06-11 22:01:25', '2026-06-11 22:01:25', NULL, '8a247364a872c223b37682c6496d3920', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD4QYHYRTK0SGRZCBSHSQ0', 'T-388', '2026-06-11 22:01:43', '2026-06-11 22:01:43', NULL, 'cc238ee850d4d0ed90d05b9a42c8d506', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD6FQMXMQQQ877KMNCK6QC', 'T-389', '2026-06-11 22:01:57', '2026-06-11 22:01:57', NULL, 'bb0657e304f4ad1445d756ef7170c62d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHD8F2NBEFZNPKSJ9W253J0', 'T-390', '2026-06-11 22:02:14', '2026-06-11 22:02:14', NULL, '35453f0d8c7e0fb89c243481f870de03', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDAK04ZA0PBT69ZWNBXPSR', 'T-391', '2026-06-11 22:02:31', '2026-06-11 22:02:31', NULL, '0647245b4da95532bb3abd6f20cd87de', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDC7B2MFQZVR1K9E30FXDR', 'T-392', '2026-06-11 22:02:44', '2026-06-11 22:02:44', NULL, 'a1612ff75df3d134b8e05c716d24ebfe', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDE5A3965GNRFS5WPZW878', 'T-393', '2026-06-11 22:03:00', '2026-06-11 22:03:00', NULL, 'f8f29fb8b31cc8afdd8af989c3f711f1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDGPXQN31NNRPJ00PFRAG4', 'T-394', '2026-06-11 22:03:21', '2026-06-11 22:03:21', NULL, 'f9cee5dc96cedfabc3eaaf7352e732c8', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBHDH8TE9MJBXZ4GQ5YT10JM', 'T-395', '2026-06-11 22:03:26', '2026-06-11 22:03:26', NULL, '382a5742e32da0f38c1e143715a0b656', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBJAXQHCKHS8ZSDZNM9NH7QM', 'T-396', '2026-06-12 00:11:50', '2026-06-12 00:11:50', NULL, '20577481e56f82bb0df9e56a266303c9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBJM6XXQZ3EMGRC13XRYVBEM', 'T-397', '2026-06-12 00:52:25', '2026-06-12 00:52:25', NULL, 'bf89e25b384be60faa65e9eb1ec2fab9', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMV7Y13PAYKZC0WB4FQXKC', 'T-398', '2026-06-12 03:15:00', '2026-06-12 03:15:00', NULL, 'd0ed46ee279c148f1d76683e86e0d4aa', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKMXSVCE98K1H76N00TYCQR', 'T-399', '2026-06-12 03:15:21', '2026-06-12 03:15:21', NULL, 'eacf3522aeb2bccbaf006c9703b9794d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKN09R21H3AWR2Q2ZTSGNSW', 'T-400', '2026-06-12 03:15:41', '2026-06-12 03:15:41', NULL, 'a7971f428145d04ae82d4cd89f3eeb9d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKN2MP35NPPK1BRDYY2M428', 'T-401', '2026-06-12 03:16:00', '2026-06-12 03:16:00', NULL, '29980034db5fe381473b156ece7d8a1a', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKN4QVFVE51MY2N0CWCVXHM', 'T-402', '2026-06-12 03:16:18', '2026-06-12 03:16:18', NULL, 'c65717bf20ea6886ef7a86f5cb4b2929', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKP67X1Y1FEE9T5R0E5DA9C', 'T-403', '2026-06-12 03:20:52', '2026-06-12 03:20:52', NULL, '372a686b214820b5d093b11b9f3d57a5', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKP8KFAF526ZNXBQS98DPPG', 'T-404', '2026-06-12 03:21:11', '2026-06-12 03:21:11', NULL, 'c479a9dea6687b553bc638a9849cc5de', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPAZR4XEV8YW3PVR2XJBFC', 'T-405', '2026-06-12 03:21:31', '2026-06-12 03:21:31', NULL, 'e4e1695f838b8fbf02aae49a6f2df4fe', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPD85PFBPJJTQ0WS3PWWXR', 'T-406', '2026-06-12 03:21:49', '2026-06-12 03:21:49', NULL, '689352238d2050a3769b2a9613f0a793', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBKPFTC7H5NY0XHBTEGF8XQ4', 'T-407', '2026-06-12 03:22:10', '2026-06-12 03:22:10', NULL, '129d2b3c31022d53025a3e28169a060e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBN3VTK2MYQQ173MSJN6E1DM', 'T-408', '2026-06-12 06:40:25', '2026-06-12 06:40:25', NULL, '0353aaab57a40900b00883fb12e135f7', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBN3VYR84023Z5XFEX9DS0S0', 'T-409', '2026-06-12 06:40:26', '2026-06-12 06:40:26', NULL, '69c280319335a8d0ef646998f4531e96', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3EZC7AJANXZVF3D91QYWM', 'T-410', '2026-06-12 08:58:29', '2026-06-12 08:58:29', NULL, '4f726d1a66d38d14018a62c8d24ffe62', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3GM6V0RZBY2PXE9ZQFR88', 'T-411', '2026-06-12 08:58:42', '2026-06-12 08:58:42', NULL, 'bdd3f8b13caf25677ac661ec29599488', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3J7TXMG0F9E2WQDENPVJG', 'T-412', '2026-06-12 08:58:55', '2026-06-12 08:58:55', NULL, 'e56d50bc6fc04f6d646f22c104e63183', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3KRWM65MD3DS251NN9YX0', 'T-413', '2026-06-12 08:59:08', '2026-06-12 08:59:08', NULL, 'b5247ea05c106500682be978a47e61ee', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3P8YERJ5R7ENSD675BX00', 'T-414', '2026-06-12 08:59:28', '2026-06-12 08:59:28', NULL, '7d973f16c99441dbba0f8df89a665b32', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBP3P91QQQDT5J50F52FPCKM', 'T-415', '2026-06-12 08:59:28', '2026-06-12 08:59:28', NULL, 'a2dea9268d44b9e8a1fd746bbb947c92', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBPQ8QNGJFFK7G24CBWQAR2C', 'T-416', '2026-06-12 10:25:00', '2026-06-12 10:25:00', NULL, 'f8c2a125e661607d5dd0c73cd2c3f2ab', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ4BYD4STCKCY8JNKF23Q4W', 'T-417', '2026-06-12 11:22:15', '2026-06-12 11:22:15', NULL, '15aa9b25417162126cbcde174d3537da', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBQ595H08JFTRFSR90GSZQ0G', 'T-418', '2026-06-12 11:26:14', '2026-06-12 11:26:14', NULL, '000e07ae64b08273a2d2d9f8a77d193f', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FBTTMGKSYMTF8M1KQWTG774W', 'T-419', '2026-06-12 19:58:58', '2026-06-12 19:58:58', NULL, 'd2b01a2c3d1ce24cc863ac6d9d814d3d', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FC2XY1T85A65YY9SG25VVEY4', 'T-420', '2026-06-13 14:51:51', '2026-06-13 14:51:51', NULL, '16ea4c9353a56798b894ab3d85fb7b56', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG3T4A7CYPTG535KVAVH6C', 'T-421', '2026-06-14 15:29:23', '2026-06-14 15:29:23', NULL, 'a28eed4b57104034c5216344a326195c', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDKX4CVHWVGDAJC6X09602M', 'T-422', '2026-06-14 15:45:57', '2026-06-14 15:45:57', NULL, '5701c634f5737a2ba1612deab8df7049', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDM61KAA3GV3CVTE8PAZ8N0', 'T-423', '2026-06-14 15:47:10', '2026-06-14 15:47:10', NULL, 'ce7dfb8b9bd088b4c2e8ddfacc8d2124', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCENXW1VF7VXQX64982X171R', 'T-424', '2026-06-14 18:14:36', '2026-06-14 18:14:36', NULL, 'ecbed75dcd34c39bcbd86e60d6f4a2c2', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCENXXZFBZ0HVD1VCW4ZASCC', 'T-425', '2026-06-14 18:14:36', '2026-06-14 18:14:36', NULL, 'b09fc55465f7de02cf98f69c99e0e6e3', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP60AS6AF654SWA189A5ZR', 'T-426', '2026-06-14 18:15:42', '2026-06-14 18:15:42', NULL, '91f50c6e38332047f8619db428d4b376', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP642C8ZZ1T20RXQQ3143M', 'T-427', '2026-06-14 18:15:43', '2026-06-14 18:15:43', NULL, '55a937def177025ef6b61a222d88b142', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP67ZHMBFW0GRH9JKDMQ7R', 'T-428', '2026-06-14 18:15:44', '2026-06-14 18:15:44', NULL, '185e7d3ce8529fbe1f4543f4c635f200', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP6BDBHGMK9VCRV6JQ00TW', 'T-429', '2026-06-14 18:15:45', '2026-06-14 18:15:45', NULL, 'ef268614a716384a7597b3e304a6c176', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCEP6EVN9S35T02MHA2AS7YW', 'T-430', '2026-06-14 18:15:46', '2026-06-14 18:15:46', NULL, 'd6333df4da5b7ab8958149a9f0d17974', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCGJ30V24BJB001GZCR5QKTC', 'T-431', '2026-06-14 22:37:27', '2026-06-14 22:37:27', NULL, 'b886820e87f5abd329126bf5e9f1a3da', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9ER04JVFW8CN3JW1AWYA8', 'T-432', '2026-06-15 07:18:58', '2026-06-15 07:18:58', NULL, '71f3e4c95f66abc5e7e5820548201e41', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9F446MZFXVHH65Q6CKTPM', 'T-433', '2026-06-15 07:19:01', '2026-06-15 07:19:01', NULL, 'b9a361f2c29b286bc808dbea57a05a7e', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9FHC8VX50759X35VNER1R', 'T-434', '2026-06-15 07:19:04', '2026-06-15 07:19:04', NULL, '403c4c8aa5659bb379cb8add9dff800b', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9FYDEXCM15FXTER032K84', 'T-435', '2026-06-15 07:19:08', '2026-06-15 07:19:08', NULL, '7c2ed604aecea99b742b341166cf2257', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
INSERT INTO ticket_idmap (record_id, ticket_id, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCM9GAQ2G0KCVMZS67SK3324', 'T-436', '2026-06-15 07:19:11', '2026-06-15 07:19:11', NULL, '3114ab57de9b03aa1e745af01001eee1', 2) ON CONFLICT(record_id) DO UPDATE SET ticket_id=excluded.ticket_id, updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_idmap.updated_at OR (excluded.updated_at = ticket_idmap.updated_at AND excluded.hash > ticket_idmap.hash);
|
||||
@@ -0,0 +1,132 @@
|
||||
-- Auto-generated by migrate-ids (D-26). CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15.
|
||||
-- pql:created_by: migrate-ids
|
||||
-- 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);
|
||||
@@ -0,0 +1,2 @@
|
||||
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG3T4A7CYPTG535KVAVH6C', 'multi-window', '2026-06-14 15:29:27', '2026-06-14 15:29:27', NULL, 'ce03f8eb534bc45e2c1c12e9b30a2c30', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||
INSERT INTO ticket_labels (ticket_record_id, label, created_at, updated_at, deleted_at, hash, canonical_version) VALUES ('06FCDG3T4A7CYPTG535KVAVH6C', 'ipc', '2026-06-14 15:29:28', '2026-06-14 15:29:28', NULL, 'e6789b66db5c1de85daf6fc8d3474449', 2) ON CONFLICT(ticket_record_id, label) DO UPDATE SET updated_at=excluded.updated_at, deleted_at=excluded.deleted_at, hash=excluded.hash, canonical_version=excluded.canonical_version WHERE excluded.updated_at > ticket_labels.updated_at OR (excluded.updated_at = ticket_labels.updated_at AND excluded.hash > ticket_labels.hash);
|
||||
@@ -0,0 +1,132 @@
|
||||
-- Auto-generated by migrate-ids (D-26). CREATE TABLE statements
|
||||
-- for the planning schema; per-table dir keeps the changelog
|
||||
-- self-describing per D-15.
|
||||
-- pql:created_by: migrate-ids
|
||||
-- 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);
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
+1904
-44
File diff suppressed because it is too large
Load Diff
@@ -2,344 +2,99 @@
|
||||
|
||||
This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository.
|
||||
|
||||
## Project Overview
|
||||
## What clide is
|
||||
|
||||
Clide is a TUI IDE wrapper for Claude Code CLI, designed to be Claude-centric with VSCode-familiar keybindings. See `docs/tui-ide-spec.md` for full specification.
|
||||
An IDE for Claude Code CLI. Single Flutter package at the repo root.
|
||||
|
||||
### Design Principles
|
||||
- **Claude-centric**: Claude Code is the primary workspace, always visible
|
||||
- **Contextual panels**: Editor/Diff/Terminal appear only when needed
|
||||
- **Alt-key shortcuts**: Alt-based keybindings don't interfere with input fields
|
||||
- **Responsive**: Works on 13" laptop to widescreen monitors
|
||||
- **State preservation**: Hiding panels preserves all state (never destroy widgets)
|
||||
- **`lib/`** — all Dart code. Subsystem handlers (`lib/src/daemon/`, `lib/src/pty/`, `lib/src/ipc/`, `lib/src/git/`, `lib/src/pql/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), and the extension framework (`lib/extension/`). The Flutter app hosts the IPC server in-process (D-56). PTY spawning uses Dart FFI `posix_openpt()` + `posix_spawn()` directly.
|
||||
- **[`pql`](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
||||
|
||||
## Tech Stack
|
||||
Claude session persistence is `--resume <session-id>` against Claude Code's transcript files (D-77, superseding the original tmux-backed D-41) — the app re-attaches on restart, no tmux required. Native rendering — markdown, canvas, graph — is Dart/Flutter (`CustomPaint` + widgets), not third-party packages.
|
||||
|
||||
| Component | Library | Version |
|
||||
|-----------|---------|---------|
|
||||
| Runtime | Python | 3.12+ |
|
||||
| TUI Framework | Textual | latest |
|
||||
| CLI | Typer | latest |
|
||||
| Data Validation | Pydantic | v2 (strict mode) |
|
||||
| Settings | pydantic-settings | latest |
|
||||
| Database | SQLModel + SQLite | latest |
|
||||
| Web Server | FastAPI + uvicorn | latest |
|
||||
| Testing | pytest + pytest-asyncio + pytest-textual-snapshot | latest |
|
||||
| Extensions | pluggy | latest |
|
||||
Design doc: [`docs/initial-plan.md`](docs/initial-plan.md). Decisions: [`governance/`](governance/) (`D-NNN` confirmed, `Q-NNN` open, `R-NNN` rejected — see [`governance/README.md`](governance/README.md)). Python Textual predecessor under [`legacy/`](legacy/).
|
||||
|
||||
## Development Commands
|
||||
## Guardrails
|
||||
|
||||
### Clide (TUI)
|
||||
These are load-bearing. Violating any means the design is wrong, not the rule.
|
||||
|
||||
```bash
|
||||
make setup # Create venv, install deps
|
||||
make run # Run application
|
||||
make test # Run all tests
|
||||
make test-single # Run single test (TEST=path::test_name)
|
||||
make typecheck # Run mypy
|
||||
make lint # Run ruff check
|
||||
make format # Run ruff format
|
||||
make build # Build for current platform
|
||||
```
|
||||
- **Flutter desktop is the host. No Electron, ever.** Web target may work as a happy accident — don't compromise desktop fidelity for it. If we ship a web build at all, prefer Flutter's **WebAssembly (CanvasKit/Skwasm) compile** over the JS/HTML renderer. `xterm.dart` is the terminal renderer; markdown, canvas, graph are custom `CustomPaint`/widget components.
|
||||
- **Single process.** The Flutter app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), extensions. No separate daemon binary (D-56 dissolved it).
|
||||
- **CLI-first, not MCP.** Claude talks via Bash (`clide ...`), matching pql's contract. See [`D-1`](governance/decisions/architecture.md#d-1-cli-first-not-mcp).
|
||||
- **Dart is the core; pql fills the query gap.** PTY spawning is native Dart FFI (`posix_openpt` + `posix_spawn`). `pql` (Go) handles vault queries. No second "core language." See [`D-5`](governance/decisions/architecture.md#d-5-dart-core-sidecar-dissolved-ptyc-as-pql-peer) (amended by D-56).
|
||||
- **Own the rendering stack.** PTY (via Dart FFI), markdown renderer, graph, canvas — all clide-owned, not pulled from opinionated packages.
|
||||
- **User/Claude parity.** Every CLI subcommand has a UI affordance, and every UI action has a CLI. See [`D-6`](governance/decisions/architecture.md#d-6-cli-and-event-surface-contract).
|
||||
- **pql: wrap, don't duplicate.** Pql logic only lives in `lib/src/pql/` (pure shell-outs). Clide owns pql's `ignore_files:` config key; it never touches pql's `.pql/` index/cache data. See [`D-3`](governance/decisions/architecture.md#d-3-pql-as-supporter-tool-clide-wraps-never-duplicates).
|
||||
- **Repo-is-the-workspace.** The git repo root is the workspace — no parallel "vault" concept.
|
||||
- **Ignore discipline.** Single knob: `ignore_files:` in `.pql/config.yaml`, ordered layering. See [`D-4`](governance/decisions/architecture.md#d-4-ignore-file-strategy).
|
||||
- **Decision discipline.** All architectural choices live in `governance/decisions/<domain>.md` as `D-NNN` records. Open questions as `Q-NNN` under `governance/questions/<domain>.md`. Rejected alternatives as `R-NNN` under `governance/rejected/<domain>.md`. Claim new IDs via `pql decisions claim D <domain> "title"`. See [`governance/README.md`](governance/README.md).
|
||||
- **No pre-existing excuse.** Solo-dev repo — every failure encountered is yours to fix, regardless of who introduced it. If `make test` is red, a golden is broken, or `flutter analyze` shows a warning when you start working, the order is: **fix it first, then your work**. If you genuinely can't fix it in scope (separate ticket, large sweep, missing context), stop and surface it before continuing — don't push on top of broken state. "It was already broken" is not a reason to add more on top.
|
||||
|
||||
### clide-web (Web Server)
|
||||
|
||||
```bash
|
||||
cd clide-web/
|
||||
make setup # Create venv, install deps, run setup wizard
|
||||
make run # Run the web server (foreground)
|
||||
make dev # Run with auto-reload
|
||||
make start-server # Start systemd service
|
||||
make stop-server # Stop systemd service
|
||||
make restart-server # Restart systemd service
|
||||
make status-server # Show service status
|
||||
make logs-server # Tail service logs
|
||||
```
|
||||
|
||||
## Panel Architecture
|
||||
## Repo layout
|
||||
|
||||
```
|
||||
┌─────────────────┬─────────────────────────┬──────────────────┐
|
||||
│ panel-sidebar │ panel-workspace (60%) │ panel-context │
|
||||
│ │ [Editor][Diff][Terminal]│ │
|
||||
│ [Files][Git] │ (hidden when inactive) │ [Problems][TODOs]│
|
||||
│ [Tree] ├─────────────────────────┤ [Jira] │
|
||||
│ │ │ │
|
||||
│ (content area) │ panel-claude │ (content area) │
|
||||
│ │ (40% when workspace │ │
|
||||
│ │ visible, else 100%) │ │
|
||||
├─────────────────┤ ├──────────────────┤
|
||||
│ ⎇ main ▾ │ │ [⚠ 3][✓12][Jira]│
|
||||
└─────────────────┴─────────────────────────┴──────────────────┘
|
||||
lib/
|
||||
main.dart # Flutter app entry point
|
||||
app.dart # Root layout, workspace, panels
|
||||
clide.dart # Barrel: shared types (IPC envelope, pane kinds, etc.)
|
||||
src/ # Core subsystems (IPC server, PTY, git, files, pql, panes, editor)
|
||||
kernel/ # Kernel services (theme, i18n, settings, panels, commands, focus)
|
||||
builtin/ # Built-in extensions (claude, editor, files, git, terminal, etc.)
|
||||
widgets/ # Custom widget primitives (no Material/Cupertino)
|
||||
extension/ # Extension contract and registration
|
||||
lua/ # Lua runtime support (Tier 6)
|
||||
test/ # All tests (core subsystems + widgets + goldens + a11y)
|
||||
assets/ # Fonts, themes, grammars, licenses, logo
|
||||
linux/, macos/, web/ # Flutter platform directories
|
||||
native/ # Vendored native libs (libtree-sitter.so, dugite)
|
||||
governance/ # D/Q/R records (decisions/, questions/, rejected/ subdirs)
|
||||
docs/ # Design docs, wireframes
|
||||
legacy/ # Python Textual clide v1.2 (frozen)
|
||||
```
|
||||
|
||||
## Project Structure
|
||||
## Dependencies & supply chain
|
||||
|
||||
- **Prefer-zero-deps.** Flutter-SDK widgets first; third-party packages need justification. What stays is exact-pinned in `pubspec.yaml` (no caret ranges). Advisories reviewed before every bump; `pubspec.lock` committed.
|
||||
- **Document every bundled dependency.** Listed in [`assets/licenses.yaml`](assets/licenses.yaml) with name, kind, version, homepage, license, and purpose. Adding a dep is a two-step commit: add the artefact **and** the `licenses.yaml` entry. See [`D-42`](governance/decisions/tooling.md#d-42-bundled-dependencies-documented-in-licensesyaml).
|
||||
- **Native deps (dugite, libtree-sitter):** vendored in `native/`, pinned by SHA. Bumps follow the same advisory-review + `licenses.yaml` rule.
|
||||
|
||||
## Commands
|
||||
|
||||
```
|
||||
clide/
|
||||
├── clide/ # Package source
|
||||
│ ├── __init__.py
|
||||
│ ├── __main__.py
|
||||
│ ├── app.py # Main App, layout, keybindings
|
||||
│ ├── cli.py # Typer entry point
|
||||
│ ├── controllers/ # Domain logic (no UI)
|
||||
│ ├── widgets/ # UI components
|
||||
│ │ ├── panels/ # Main layout containers
|
||||
│ │ └── components/ # Reusable UI pieces
|
||||
│ ├── models/ # Pydantic data models
|
||||
│ ├── services/ # Background task logic
|
||||
│ ├── themes/ # Theme system
|
||||
│ ├── extensions/ # Plugin system
|
||||
│ └── helpers/ # Utility functions
|
||||
│
|
||||
├── tests/
|
||||
│ ├── conftest.py
|
||||
│ ├── harnesses/
|
||||
│ │ ├── app_harness.py
|
||||
│ │ └── controller_harness.py
|
||||
│ ├── unit/
|
||||
│ ├── integration/
|
||||
│ └── snapshots/
|
||||
│
|
||||
├── .config/ # User config (gitignored)
|
||||
│ ├── settings.toml # User settings
|
||||
│ └── themes/ # Custom user themes
|
||||
│ └── my-theme.toml
|
||||
├── docs/
|
||||
│ ├── tui-ide-spec.md # Full UI/UX specification
|
||||
│ ├── web-deployment.md # Web deployment architecture
|
||||
│ └── ARCHITECTURE.md # Framework best practices
|
||||
├── pyproject.toml
|
||||
└── Makefile
|
||||
│
|
||||
clide-web/ # Web server package (wraps clide)
|
||||
├── clide_web/
|
||||
│ ├── server.py # FastAPI app, routes, WebSocket handler
|
||||
│ ├── sessions.py # tmux session manager
|
||||
│ ├── pty_bridge.py # PTY ↔ WebSocket bridge
|
||||
│ ├── config.py # Pydantic settings with DB overlay
|
||||
│ ├── setup_wizard.py # Interactive first-run config
|
||||
│ └── static/
|
||||
│ ├── index.html # HTML page (toolbar + xterm.js)
|
||||
│ └── vendor/ # Vendored xterm.js (offline)
|
||||
├── pyproject.toml
|
||||
└── Makefile
|
||||
make run # launch Flutter desktop app
|
||||
make analyze # flutter analyze
|
||||
make format # dart format --set-exit-if-changed
|
||||
make test # fast test suite (analyze + format + unit + widget + golden)
|
||||
make test-core # core subsystem tests (IPC, PTY, git, pane registry)
|
||||
make test-a11y # accessibility contract tests
|
||||
make test-integration# real app boot integration tests
|
||||
make build-linux # flutter build linux
|
||||
make build-macos # flutter build macos
|
||||
make push-check # pre-push gate: decisions + core + fast tests + a11y
|
||||
make hooks # install the repo's git hooks (one-time setup)
|
||||
make clean # remove build artefacts
|
||||
```
|
||||
|
||||
## Key Patterns
|
||||
One-time setup on a fresh clone: `make hooks && flutter pub get` once Flutter is installed.
|
||||
|
||||
### Panel Visibility (Hide, Don't Destroy)
|
||||
### Tooling discipline
|
||||
|
||||
```python
|
||||
def toggle_workspace(self, visible: bool) -> None:
|
||||
workspace = self.query_one("#panel-workspace")
|
||||
workspace.display = visible # Preserves all child state
|
||||
The `make` targets above are the entry points — run them, not the scripts they wrap. Check the changelog with `make changelog-gate`, never `ci/changelog_gate.sh` directly; same for `analyze`/`format`/`test`/`push-check`. The `make` layer sets up the environment and stays correct if a script moves.
|
||||
|
||||
claude = self.query_one("#panel-claude")
|
||||
claude.styles.height = "40%" if visible else "100%"
|
||||
```
|
||||
Shell hygiene (keeps commands inside the permission allowlist, so they don't get denied mid-task):
|
||||
- **Working directory is the repo root already** — don't prepend `cd /…/clide` or pass `git -C`. Just run the command.
|
||||
- **One command per invocation** — no `&&`/`;` chaining and no multiple greps/echos in one call. The only exception is the `git commit -F` HEREDOC.
|
||||
- Prefer the Read/Edit/Grep tools over `cat`/`sed`/`grep` for inspecting files.
|
||||
|
||||
### Background Tasks
|
||||
## Git workflow
|
||||
|
||||
Use `@work` decorator for non-blocking operations:
|
||||
Commit and push directly to `main` for routine work — this is a solo-dev repo and does not use a branch-first / feature-branch flow. Do **not** create a working branch just to land a change. (This overrides the generic "branch before committing on the default branch" assistant default.) The usual safety rules still hold: never `--no-verify`, never force-push `main`, and let the pre-push gate run.
|
||||
|
||||
```python
|
||||
@work(thread=True)
|
||||
def refresh_git_status(self) -> None:
|
||||
result = subprocess.run(["git", "status", "--porcelain"], ...)
|
||||
self.call_from_thread(self.update_git_view, result.stdout)
|
||||
```
|
||||
The pre-commit hook auto-exports and stages `.pql/changelog/` (the pql ticket DB) on every commit — don't hand-stage it. A ticket change only persists if the turn makes at least one commit; with no commit the hook never fires and a later branch switch can drop it.
|
||||
|
||||
### Reactive State
|
||||
## Changelog discipline
|
||||
|
||||
```python
|
||||
class ClideApp(App):
|
||||
current_file: reactive[str | None] = reactive(None)
|
||||
workspace_visible: reactive[bool] = reactive(False)
|
||||
problem_count: reactive[int] = reactive(0)
|
||||
todo_count: reactive[int] = reactive(0)
|
||||
compact_mode: reactive[bool] = reactive(False)
|
||||
```
|
||||
[Keep a Changelog 1.1.0](https://keepachangelog.com/en/1.1.0/). Every user-visible commit adds an entry under `## [Unreleased]` in [`CHANGELOG.md`](CHANGELOG.md). Cutting a release means moving Unreleased entries under a new dated version heading **and** bumping `pubspec.yaml` `version:` in the same commit — see [`.claude/skills/git-commit/SKILL.md`](.claude/skills/git-commit/SKILL.md) for the full rule.
|
||||
|
||||
### Pydantic Models (Strict + Frozen)
|
||||
## Open questions
|
||||
|
||||
```python
|
||||
from pydantic import BaseModel, ConfigDict
|
||||
|
||||
class GitChange(BaseModel):
|
||||
model_config = ConfigDict(strict=True, frozen=True)
|
||||
path: str
|
||||
status: Literal["added", "modified", "deleted", "untracked", "renamed"]
|
||||
staged: bool
|
||||
```
|
||||
|
||||
### Controller → Widget Communication
|
||||
|
||||
Controllers emit Textual messages; widgets subscribe:
|
||||
|
||||
```python
|
||||
# In controller
|
||||
class GitStatusUpdated(Message):
|
||||
def __init__(self, status: GitStatus) -> None:
|
||||
self.status = status
|
||||
super().__init__()
|
||||
|
||||
self.post_message(GitStatusUpdated(status))
|
||||
|
||||
# In widget
|
||||
def on_git_status_updated(self, event: GitStatusUpdated) -> None:
|
||||
self.refresh_view(event.status)
|
||||
```
|
||||
|
||||
## Theme System
|
||||
|
||||
### Default Theme: Summer Night
|
||||
|
||||
Based on [jackw01/summer-night-vscode-theme](https://github.com/jackw01/summer-night-vscode-theme):
|
||||
|
||||
```python
|
||||
SUMMER_NIGHT = ThemeColors(
|
||||
primary="#00a3d2", # cyan
|
||||
secondary="#00a9b9", # teal
|
||||
accent="#fa5f8b", # pink
|
||||
background="#21262f", # mono_8
|
||||
surface="#393e48", # mono_7
|
||||
panel="#292e38",
|
||||
foreground="#e2e8f5", # mono_1
|
||||
success="#00ab9a", # green
|
||||
warning="#d08447", # orange
|
||||
error="#f06c6f", # red
|
||||
)
|
||||
```
|
||||
|
||||
### Built-in Themes (22 total)
|
||||
|
||||
| Category | Themes |
|
||||
|----------|--------|
|
||||
| Core | summer-night (default), summer-day |
|
||||
| Popular | one-dark, one-dark-pro, one-light, dracula, nord, gruvbox-dark, gruvbox-light |
|
||||
| GitKraken | one-dark-teal, gamma |
|
||||
| Seasonal - Winter | winter-is-coming, monokai-winter |
|
||||
| Seasonal - Fall | fall, dark-autumn |
|
||||
| Seasonal - Halloween | all-hallows-eve, halloween |
|
||||
| Seasonal - Christmas | christmas, santa-baby |
|
||||
| Hacker | pro-hacker, hacker-style |
|
||||
| Bonus | houston |
|
||||
|
||||
### Theme Definition
|
||||
|
||||
```python
|
||||
class ThemeColors(BaseModel):
|
||||
model_config = ConfigDict(strict=True, frozen=True)
|
||||
primary: str # Main accent
|
||||
secondary: str # Secondary accent
|
||||
accent: str # Highlight accent
|
||||
background: str # Main background
|
||||
surface: str # Elevated surfaces
|
||||
panel: str # Panel backgrounds
|
||||
foreground: str # Primary text
|
||||
success: str # Success/green
|
||||
warning: str # Warning/yellow
|
||||
error: str # Error/red
|
||||
|
||||
class ThemeDefinition(BaseModel):
|
||||
name: str # Identifier (e.g., "summer-night")
|
||||
display_name: str # Human-readable name
|
||||
dark: bool # Dark or light theme
|
||||
colors: ThemeColors
|
||||
```
|
||||
|
||||
### Custom Themes
|
||||
|
||||
Users can add themes in `.config/themes/`:
|
||||
|
||||
```toml
|
||||
# .config/themes/my-theme.toml
|
||||
name = "my-theme"
|
||||
display_name = "My Custom Theme"
|
||||
dark = true
|
||||
|
||||
[colors]
|
||||
primary = "#007acc"
|
||||
secondary = "#3c3c3c"
|
||||
accent = "#0e639c"
|
||||
background = "#1e1e1e"
|
||||
surface = "#252526"
|
||||
panel = "#2d2d30"
|
||||
foreground = "#d4d4d4"
|
||||
success = "#4ec9b0"
|
||||
warning = "#dcdcaa"
|
||||
error = "#f44747"
|
||||
```
|
||||
|
||||
### Theme Switching
|
||||
|
||||
- Keybinding: `Ctrl+K Ctrl+T`
|
||||
- Settings: `theme = "summer-night"` in ClideSettings
|
||||
- Runtime: `app.theme = "dracula"`
|
||||
|
||||
## Keybindings (Alt-based)
|
||||
|
||||
| Action | Binding |
|
||||
|--------|---------|
|
||||
| Quit | `Alt+Q` |
|
||||
| Command palette | `Alt+P` |
|
||||
| Quick open | `Alt+O` |
|
||||
| Toggle left sidebar | `Alt+B` |
|
||||
| Toggle right sidebar | `Alt+Shift+B` |
|
||||
| Toggle terminal | `` Alt+` `` |
|
||||
| Focus Claude | `Alt+1` |
|
||||
| Focus Editor | `Alt+2` |
|
||||
| Focus Terminal | `Alt+3` |
|
||||
| Toggle compact mode | `Alt+C` |
|
||||
| Git panel | `Alt+G` |
|
||||
| Problems panel | `Alt+M` |
|
||||
| Select theme | `Alt+T` |
|
||||
| Save file | `Alt+S` |
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings are loaded from multiple sources (in priority order):
|
||||
1. Environment variables (`CLIDE_*`)
|
||||
2. `.config/settings.toml`
|
||||
3. Defaults in ClideSettings
|
||||
|
||||
```python
|
||||
class ClideSettings(BaseSettings):
|
||||
model_config = SettingsConfigDict(
|
||||
env_prefix="CLIDE_",
|
||||
env_file=".env",
|
||||
extra="ignore",
|
||||
)
|
||||
|
||||
theme: str = "summer-night"
|
||||
jira_enabled: bool = False
|
||||
jira_cli_path: str = "jira"
|
||||
|
||||
panels: PanelConfig = PanelConfig()
|
||||
keybindings: KeybindingsConfig = KeybindingsConfig()
|
||||
```
|
||||
|
||||
## Documentation Links
|
||||
|
||||
### Core Stack
|
||||
- [Textual Documentation](https://textual.textualize.io/)
|
||||
- [Textual Testing Guide](https://textual.textualize.io/guide/testing/)
|
||||
- [Textual Themes Guide](https://textual.textualize.io/guide/design/)
|
||||
- [Typer Documentation](https://typer.tiangolo.com/)
|
||||
- [Pydantic v2 Documentation](https://docs.pydantic.dev/latest/)
|
||||
- [Pydantic Settings](https://docs.pydantic.dev/latest/concepts/pydantic_settings/)
|
||||
|
||||
### Testing
|
||||
- [pytest-asyncio](https://pytest-asyncio.readthedocs.io/en/latest/)
|
||||
- [pytest-textual-snapshot](https://github.com/Textualize/pytest-textual-snapshot)
|
||||
|
||||
### Extensions
|
||||
- [Pluggy Documentation](https://pluggy.readthedocs.io/)
|
||||
|
||||
### Build
|
||||
- [PyInstaller Documentation](https://pyinstaller.org/)
|
||||
- [Gitea Actions](https://docs.gitea.com/usage/actions/overview)
|
||||
|
||||
### Theme References
|
||||
- [Summer Night VSCode Theme](https://github.com/jackw01/summer-night-vscode-theme)
|
||||
Open questions live under [`governance/questions/`](governance/questions/).
|
||||
|
||||
+233
@@ -0,0 +1,233 @@
|
||||
# Contributing to clide
|
||||
|
||||
clide is an IDE for the Claude Code CLI, built as a single Flutter
|
||||
package at the repo root. This guide is for people working on clide
|
||||
itself.
|
||||
|
||||
For day-to-day model-driven work in the codebase, see
|
||||
[`CLAUDE.md`](CLAUDE.md) — it documents the same guardrails from the
|
||||
agent's point of view and is the place to look for "why is the code
|
||||
shaped like this?"
|
||||
|
||||
## One-time setup
|
||||
|
||||
```
|
||||
git clone git@github.com:postmeridiem/clide.git
|
||||
cd clide
|
||||
make hooks && flutter pub get
|
||||
```
|
||||
|
||||
`make hooks` points `core.hooksPath` at [`.githooks/`](.githooks). The
|
||||
load-bearing one is **pre-push** (runs `make push-check`); the rest
|
||||
(pre-commit, post-checkout/merge/rewrite) are housekeeping. Flutter must
|
||||
be on the stable channel and on `$PATH`.
|
||||
|
||||
If you haven't used [pql](https://github.com/postmeridiem/pql) before,
|
||||
install it and run `pql init` once in the repo root. pql is a hard
|
||||
dependency for the governance + ticket workflow described below.
|
||||
|
||||
## The commands you'll actually use
|
||||
|
||||
```
|
||||
make run # launch the desktop app
|
||||
make verify # no-tests sweep — analyze + format + decisions + changelog gate
|
||||
make test # fast dev loop — analyze + format + unit + widget + golden, NO coverage, parallel (~20s)
|
||||
make test-coverage # same suite WITH coverage (writes coverage/lcov.info; used by the gate)
|
||||
make test-a11y # WCAG-AA contrast + keyboard traversal contracts
|
||||
make push-check # the pre-push gate; what CI runs (runs test-coverage, not the no-coverage test)
|
||||
make build-linux # release artefact for the host platform
|
||||
```
|
||||
|
||||
`make verify` is the lightweight "are the gates green?" check for
|
||||
mid-edit iteration. `make push-check` is the full pre-push pipeline
|
||||
(verify + every test suite + coverage gate).
|
||||
|
||||
`make` with no target prints the full list. `make test-integration`
|
||||
boots a real app process and is slow; reserve it for the rare change
|
||||
that touches startup wiring.
|
||||
|
||||
The pre-push hook runs `make push-check` automatically. Don't bypass
|
||||
it with `--no-verify` — fix the underlying issue and create a new
|
||||
commit. Pre-push includes:
|
||||
|
||||
- `flutter analyze` (zero warnings)
|
||||
- `dart format --set-exit-if-changed`
|
||||
- the core (`dart test`) suite
|
||||
- the unit/widget/golden/a11y suites, instrumented for coverage
|
||||
(`make test-coverage` — the a11y suite runs here, not as a separate pass)
|
||||
- a coverage floor read from `coverage_floor:` in `pubspec.yaml`
|
||||
(currently 95 %; ratchets up only — see
|
||||
[D-66](governance/decisions/testing.md#d-66))
|
||||
- `CHANGELOG.md` `[Unreleased]` bullets ≤ 60 words each
|
||||
|
||||
## The C `clide` shell client
|
||||
|
||||
`clide` (the binary) is a ~250 LOC C program in
|
||||
[`native/clide-cli/clide.c`](native/clide-cli/clide.c) that talks to
|
||||
the running Flutter app's IPC socket so Claude (and you) can drive
|
||||
clide from any shell. It walks CWD up to the workspace's `.git`,
|
||||
computes the same FNV-1a 64-bit hash the Dart side uses (per D-70),
|
||||
opens the per-workspace socket, and sends argv across the wire under
|
||||
a sentinel `_argv` cmd. The argv parser lives in Dart
|
||||
([`lib/src/cli/argv_to_request.dart`](lib/src/cli/argv_to_request.dart)),
|
||||
so the C side stays a dumb pipe.
|
||||
|
||||
Build it with `make clide-cli` — output lands at
|
||||
`native/<platform>/clide` (gitignored). Drop that on your PATH (or
|
||||
symlink) and `clide status` works from any directory inside a clide
|
||||
workspace once the app is running. Standard POSIX + libc only;
|
||||
pure C99; no third-party deps.
|
||||
|
||||
The cross-language hash agreement is load-bearing — if the Dart
|
||||
server and C client disagree on the socket path, every shell
|
||||
invocation fails to connect. The test suite covers it:
|
||||
[`test/ipc/paths_test.dart`](test/ipc/paths_test.dart) pins
|
||||
FNV-1a vectors against the reference, and
|
||||
[`test/cli/clide_cli_e2e_test.dart`](test/cli/clide_cli_e2e_test.dart)
|
||||
compiles the C client and exercises the full round-trip.
|
||||
|
||||
## Running clide from the shell
|
||||
|
||||
Once the desktop app is running and `clide` is on your `$PATH`, every
|
||||
shell inside the workspace can drive it. The argv grammar is fixed by
|
||||
[D-6](governance/decisions/architecture.md#d-6-cli-and-event-surface-contract)
|
||||
— every UI affordance has a CLI counterpart, and every CLI verb has a
|
||||
UI affordance.
|
||||
|
||||
```sh
|
||||
clide status # one-shot snapshot of pane state
|
||||
clide ping # round-trip health check
|
||||
clide tail --events # stream every subsystem event
|
||||
clide tail --events --filter git # stream a single subsystem
|
||||
clide pane focus editor # drive the running app
|
||||
clide panel toggle git # show/hide a panel
|
||||
clide panel resize sidebar --to 320 # absolute width (px)
|
||||
clide panel resize context --by -40 # relative nudge (px)
|
||||
clide panel resize editor --to 0.5 # editor split ratio (0.15–0.70)
|
||||
```
|
||||
|
||||
Output is JSON on stdout, one envelope per line. Streaming verbs
|
||||
(`tail --events`) keep the connection open and emit one event line per
|
||||
push; Ctrl-C cleanly closes the socket. Exit codes follow the pql
|
||||
convention ([D-68](governance/decisions/architecture.md#d-68-cli-and-mcp-surface-contracts)):
|
||||
|
||||
| Code | Meaning |
|
||||
|------|------------------------------------------------------|
|
||||
| 0 | success |
|
||||
| 64 | user error — bad argv, unknown verb, malformed flag |
|
||||
| 65 | data error — request well-formed but rejected |
|
||||
| 69 | service unavailable — no running clide for this repo |
|
||||
| 70 | internal error — dispatcher threw |
|
||||
|
||||
If `clide` exits 69, the desktop app isn't running for this workspace
|
||||
— start it with `make run` (during development) or launch the
|
||||
installed app pointed at this repo.
|
||||
|
||||
### Claude Code's `/ide` and the MCP server
|
||||
|
||||
The desktop app also runs an MCP companion server on a random
|
||||
localhost port (HTTP + Server-Sent Events, per
|
||||
[D-73](governance/decisions/architecture.md#d-73-mcp-server-transport-over-http-sse)).
|
||||
Discovery follows the Claude Code `/ide` contract: clide writes a
|
||||
JSON descriptor to `~/.claude/ide/<pid>.lock` containing the chosen
|
||||
port, the workspace root, and the protocol version. Claude Code's
|
||||
`/ide` command picks the lock for the workspace it's running in and
|
||||
connects automatically — no manual configuration. The lock is removed
|
||||
on graceful shutdown; stale locks from crashed processes are reaped
|
||||
on the next start. The implementation lives in
|
||||
[`lib/src/ipc/mcp_server.dart`](lib/src/ipc/mcp_server.dart).
|
||||
|
||||
## Decisions, questions, rejected (DQR)
|
||||
|
||||
clide tracks architectural commitments as durable records under
|
||||
[`governance/`](governance/):
|
||||
|
||||
- `decisions/<domain>.md` — confirmed decisions (`D-NNN`)
|
||||
- `questions/<domain>.md` — open questions (`Q-NNN`)
|
||||
- `rejected/<domain>.md` — rejected proposals (`R-NNN`)
|
||||
|
||||
When you make a non-trivial architectural choice, write it down:
|
||||
|
||||
```
|
||||
pql decisions claim D <domain> "short title"
|
||||
```
|
||||
|
||||
This reserves a fresh ID and tells you where to add the record. The
|
||||
[governance/README.md](governance/README.md) explains the format and
|
||||
the recommended domain list.
|
||||
|
||||
Pre-push validates that every `D-NNN` / `Q-NNN` / `R-NNN` link in
|
||||
the docs and code resolves to an actual record (`pql decisions
|
||||
validate`). Broken references fail the build.
|
||||
|
||||
## Tickets
|
||||
|
||||
All non-trivial work is tracked in `pql ticket`:
|
||||
|
||||
```
|
||||
pql ticket list --status in_progress
|
||||
pql ticket show T-NNN[,T-NNN…] # batch form on pql 1.4.33+
|
||||
pql ticket new task "title" --parent T-NNN --priority medium
|
||||
pql ticket status T-NNN in_progress
|
||||
pql ticket status T-NNN done
|
||||
```
|
||||
|
||||
A ticket exists for any change a reviewer might want to ask "why?"
|
||||
about. Bug fixes, refactors, and consultant findings all become
|
||||
tickets before the diff lands. Trivial typo fixes don't need one.
|
||||
|
||||
## Commit conventions
|
||||
|
||||
See [D-37](governance/decisions/process.md#d-37) and the bundled
|
||||
[`git-commit` skill](.claude/skills/git-commit/SKILL.md). In short:
|
||||
|
||||
- Imperative subject ≤ 70 chars, no Conventional Commits prefix
|
||||
(this isn't a Conventional Commits repo — the archived Python
|
||||
predecessor under [`legacy/`](legacy/) is, but the rebuild isn't).
|
||||
- One logical change per commit. If the subject needs "and", split it.
|
||||
- Every user-visible commit adds an entry to `CHANGELOG.md` under
|
||||
`[Unreleased]` in the right subsection (Added, Changed, Deprecated,
|
||||
Removed, Fixed, Security). Keep entries to one or two short
|
||||
sentences — the 60-word cap is enforced by `ci/changelog_gate.sh`.
|
||||
- Co-author trailer:
|
||||
`Co-Authored-By: Claude <noreply@anthropic.com>` when Claude wrote
|
||||
any of the diff.
|
||||
|
||||
Never `--amend` a commit unless explicitly asked. Never force-push
|
||||
to `main`. Never `git add -A` / `git add .` when staging — name
|
||||
files explicitly so stray secrets or build artefacts don't sneak in.
|
||||
|
||||
## Cutting a release
|
||||
|
||||
A release is a single commit:
|
||||
|
||||
1. Move all `## [Unreleased]` entries under a new `## [X.Y.Z] —
|
||||
YYYY-MM-DD` heading.
|
||||
2. Leave an empty `## [Unreleased]` skeleton at the top.
|
||||
3. Bump `pubspec.yaml` `version:` to `X.Y.Z` (no `-dev` suffix on
|
||||
the release tag; add it back on the next development commit if
|
||||
you like).
|
||||
4. Commit subject: `release vX.Y.Z`.
|
||||
|
||||
`pubspec.yaml` is the single source of truth for the version — the
|
||||
Makefile reads it for ldflag stamping and the app reads it for build
|
||||
info.
|
||||
|
||||
## What goes where
|
||||
|
||||
- **Bug, feature, sweep:** file/claim a ticket, branch, code, test,
|
||||
commit, push. Push triggers `make push-check`.
|
||||
- **Architectural decision:** `pql decisions claim`, write the
|
||||
record, then file the implementation ticket linked via
|
||||
`decision_ref`.
|
||||
- **Open question:** drop a `Q-NNN` under
|
||||
`governance/questions/<domain>.md`. Triage later.
|
||||
- **Rejected proposal:** drop an `R-NNN` under
|
||||
`governance/rejected/<domain>.md`. Future-you (or a reviewer)
|
||||
will be glad it's written down.
|
||||
|
||||
## Reporting issues
|
||||
|
||||
The public issue tracker lives at
|
||||
<https://github.com/postmeridiem/clide/issues>. The Gitea mirror is
|
||||
read-only.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Jeroen Schweitzer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,121 +1,357 @@
|
||||
.PHONY: setup run test test-single typecheck lint format build build-macos build-linux build-windows build-all clean help
|
||||
# clide — local dev targets. CI scripts in ci/ shell out to these.
|
||||
#
|
||||
# Single Flutter package at the repo root. Native supporter tools
|
||||
# (ptyc, future peers) live in their own directories.
|
||||
|
||||
PYTHON := python3.12
|
||||
VENV := .venv
|
||||
BIN := $(VENV)/bin
|
||||
TEST ?= tests/
|
||||
VERSION ?= 1.0.0
|
||||
INSTALL_DIR ?= $(HOME)/.local/bin
|
||||
|
||||
# Colors for output
|
||||
BLUE := \033[0;34m
|
||||
GREEN := \033[0;32m
|
||||
RESET := \033[0m
|
||||
# -- OS detection ------------------------------------------------------------
|
||||
# Maps uname to Flutter device/build targets: linux, macos, windows.
|
||||
|
||||
help:
|
||||
@printf "$(BLUE)Clide Development Commands$(RESET)\n"
|
||||
@printf "\n"
|
||||
@printf "$(GREEN)setup$(RESET) Create venv and install dependencies\n"
|
||||
@printf "$(GREEN)run$(RESET) Run the application\n"
|
||||
@printf "$(GREEN)test$(RESET) Run all tests\n"
|
||||
@printf "$(GREEN)test-single$(RESET) Run single test (TEST=path::test_name)\n"
|
||||
@printf "$(GREEN)typecheck$(RESET) Run mypy type checking\n"
|
||||
@printf "$(GREEN)lint$(RESET) Run ruff linter\n"
|
||||
@printf "$(GREEN)format$(RESET) Run ruff formatter\n"
|
||||
@printf "$(GREEN)build$(RESET) Build executable for current platform\n"
|
||||
@printf "$(GREEN)clean$(RESET) Remove build artifacts and caches\n"
|
||||
@printf "\n"
|
||||
@printf "$(BLUE)Distribution Builds$(RESET)\n"
|
||||
@printf "\n"
|
||||
@printf "$(GREEN)build-macos$(RESET) Build macOS DMG (VERSION=x.x.x)\n"
|
||||
@printf "$(GREEN)build-linux$(RESET) Build Linux AppImage (VERSION=x.x.x)\n"
|
||||
@printf "$(GREEN)build-windows$(RESET) Build Windows installer (VERSION=x.x.x)\n"
|
||||
UNAME_S := $(shell uname -s)
|
||||
ifeq ($(UNAME_S),Linux)
|
||||
FLUTTER_OS := linux
|
||||
else ifeq ($(UNAME_S),Darwin)
|
||||
FLUTTER_OS := macos
|
||||
else
|
||||
# Windows (MSYS2, Git Bash, Cygwin all report variants with "MINGW"/"MSYS"/"CYGWIN")
|
||||
FLUTTER_OS := windows
|
||||
endif
|
||||
|
||||
setup:
|
||||
@echo "Creating virtual environment..."
|
||||
$(PYTHON) -m venv $(VENV)
|
||||
@echo "Installing dependencies..."
|
||||
$(BIN)/pip install --upgrade pip
|
||||
$(BIN)/pip install -e ".[dev,build]"
|
||||
@echo "Installing pre-commit hooks..."
|
||||
$(BIN)/pre-commit install || true
|
||||
@printf "$(GREEN)Setup complete! Activate with: source $(VENV)/bin/activate$(RESET)\n"
|
||||
VERSION ?= $(shell awk -F': *' '/^version:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' pubspec.yaml)
|
||||
NAME ?= $(shell awk -F': *' '/^name:/ {gsub(/[" ]/,"",$$2); print $$2; exit}' pubspec.yaml)
|
||||
TAGLINE ?= $(shell awk -F': *' '/^tagline:/ {sub(/^tagline: *"?/,"",$$0); sub(/"$$/,"",$$0); print; exit}' pubspec.yaml)
|
||||
REPOSITORY ?= $(shell awk -F': *' '/^repository:/ {sub(/^repository: */,"",$$0); print; exit}' pubspec.yaml)
|
||||
COMMIT ?= $(shell git rev-parse --short HEAD 2>/dev/null || echo unknown)
|
||||
DATE ?= $(shell date -u +%Y-%m-%dT%H:%M:%SZ)
|
||||
|
||||
run:
|
||||
$(BIN)/python -m clide
|
||||
.PHONY: help
|
||||
help: ## Show this help.
|
||||
@awk 'BEGIN {FS = ":.*##"} /^[a-zA-Z0-9_-]+:.*##/ {printf " \033[36m%-22s\033[0m %s\n", $$1, $$2}' $(MAKEFILE_LIST)
|
||||
|
||||
test:
|
||||
$(BIN)/pytest $(TEST)
|
||||
# -- app (Flutter) -------------------------------------------------------
|
||||
|
||||
test-single:
|
||||
$(BIN)/pytest $(TEST) -v
|
||||
.PHONY: run
|
||||
run: gen-build-info clide-cli ## Launch the Flutter desktop app.
|
||||
# CLIDE_CLI_BIN points the in-app "Install clide command in PATH"
|
||||
# affordance (T-212) at the dev-tree C client; a packaged build finds it
|
||||
# beside the GUI runner in the bundle instead.
|
||||
ifeq ($(FLUTTER_OS),linux)
|
||||
CLIDE_CLI_BIN=$(CURDIR)/$(CLIDE_CLI_BIN) GDK_BACKEND=x11 LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} flutter run -d linux --dart-define=CLIDE_PROJECT=$(CURDIR)
|
||||
else
|
||||
CLIDE_CLI_BIN=$(CURDIR)/$(CLIDE_CLI_BIN) flutter run -d $(FLUTTER_OS) --dart-define=CLIDE_PROJECT=$(CURDIR)
|
||||
endif
|
||||
|
||||
test-cov:
|
||||
$(BIN)/pytest --cov=clide/clide --cov-report=html --cov-report=term
|
||||
TESTMODE_CATEGORY ?= all
|
||||
TESTMODE_TIMEOUT ?= 60
|
||||
|
||||
test-snapshots:
|
||||
$(BIN)/pytest tests/snapshots/
|
||||
.PHONY: run-testmode
|
||||
run-testmode: ## Launch ClideTestApp (TESTMODE_CATEGORY=toolchain|ipc|extensions|all).
|
||||
ifeq ($(FLUTTER_OS),linux)
|
||||
@GDK_BACKEND=x11 LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} \
|
||||
flutter run -d linux \
|
||||
--dart-define=CLIDE_PROJECT=$(CURDIR) \
|
||||
--dart-define=CLIDE_TESTMODE=$(TESTMODE_CATEGORY) 2>&1 \
|
||||
| tee /tmp/clide-testmode.log & PID=$$!; \
|
||||
(sleep $(TESTMODE_TIMEOUT) && kill $$PID 2>/dev/null) & TIMER=$$!; \
|
||||
wait $$PID 2>/dev/null; kill $$TIMER 2>/dev/null; \
|
||||
grep -q '"failed":0' /tmp/clide-testmode.log
|
||||
else
|
||||
@flutter run -d $(FLUTTER_OS) \
|
||||
--dart-define=CLIDE_PROJECT=$(CURDIR) \
|
||||
--dart-define=CLIDE_TESTMODE=$(TESTMODE_CATEGORY) 2>&1 \
|
||||
| tee /tmp/clide-testmode.log & PID=$$!; \
|
||||
(sleep $(TESTMODE_TIMEOUT) && kill $$PID 2>/dev/null) & TIMER=$$!; \
|
||||
wait $$PID 2>/dev/null; kill $$TIMER 2>/dev/null; \
|
||||
grep -q '"failed":0' /tmp/clide-testmode.log
|
||||
endif
|
||||
|
||||
test-snapshots-update:
|
||||
$(BIN)/pytest tests/snapshots/ --snapshot-update
|
||||
.PHONY: pubget
|
||||
pubget: ## flutter pub get.
|
||||
flutter pub get
|
||||
|
||||
typecheck:
|
||||
$(BIN)/mypy clide/
|
||||
.PHONY: build-check
|
||||
build-check: gen-build-info ## Verify native + Dart build compiles (no run).
|
||||
ifeq ($(FLUTTER_OS),linux)
|
||||
LD_LIBRARY_PATH=$(CURDIR)/native/linux-x64$${LD_LIBRARY_PATH:+:$$LD_LIBRARY_PATH} flutter build linux
|
||||
else
|
||||
flutter build $(FLUTTER_OS)
|
||||
endif
|
||||
|
||||
lint:
|
||||
$(BIN)/ruff check clide/ tests/
|
||||
.PHONY: analyze
|
||||
analyze: ## flutter analyze.
|
||||
flutter analyze
|
||||
|
||||
format:
|
||||
$(BIN)/ruff format clide/ tests/
|
||||
$(BIN)/ruff check --fix clide/ tests/
|
||||
.PHONY: format
|
||||
format: ## dart format --set-exit-if-changed.
|
||||
dart format --set-exit-if-changed .
|
||||
|
||||
build:
|
||||
$(BIN)/pyinstaller clide.spec --clean
|
||||
# The single bake of every build-time fact derived from pubspec.yaml
|
||||
# + git + clock. Runs implicitly as a prereq of every target that
|
||||
# compiles or executes Dart code so nobody has to remember it.
|
||||
#
|
||||
# Outputs:
|
||||
# - lib/src/build_info.g.dart — gitignored, fresh on every build.
|
||||
# Exposes `clideName`, `clideTagline`, `clideVersion`, `clideRepository`
|
||||
# (from pubspec), `clideCommit` (git short SHA), `clideDate` (UTC
|
||||
# now) for the app to read directly.
|
||||
# - assets/licenses.yaml `self.version:` — rewritten in place so the
|
||||
# bundled license manifest never drifts from pubspec. (Tracked in
|
||||
# git; the rewrite is a no-op when in sync.)
|
||||
.PHONY: gen-build-info
|
||||
gen-build-info:
|
||||
@printf '// GENERATED — do not edit. Regenerated by `make` on every\n// build/run/test (see gen-build-info in Makefile). Name / tagline /\n// version / repository come from pubspec.yaml — the single source\n// of truth. Commit + date stamp at run time.\nconst String clideName = '"'"'%s'"'"';\nconst String clideTagline = '"'"'%s'"'"';\nconst String clideVersion = '"'"'%s'"'"';\nconst String clideRepository = '"'"'%s'"'"';\nconst String clideCommit = '"'"'%s'"'"';\nconst String clideDate = '"'"'%s'"'"';\n' "$(NAME)" "$(TAGLINE)" "$(VERSION)" "$(REPOSITORY)" "$(COMMIT)" "$(DATE)" > lib/src/build_info.g.dart
|
||||
@awk -v v="$(VERSION)" '/^self:/ {in_self=1} in_self && /^[[:space:]]+version:/ {sub(/version:.*/, "version: \"" v "\""); in_self=0} {print}' assets/licenses.yaml > assets/licenses.yaml.tmp && mv assets/licenses.yaml.tmp assets/licenses.yaml
|
||||
|
||||
build-onefile:
|
||||
$(BIN)/pyinstaller \
|
||||
--name clide \
|
||||
--onefile \
|
||||
--clean \
|
||||
--noconfirm \
|
||||
clide/clide/__main__.py
|
||||
.PHONY: verify
|
||||
verify: gen-build-info analyze format decisions-validate changelog-gate ## No-tests sweep — gen-build-info + analyze + format + decisions-validate + changelog-gate. For mid-edit "are the gates green?" checks; `push-check` is the full pre-push pipeline.
|
||||
|
||||
clean:
|
||||
rm -rf $(VENV)
|
||||
rm -rf dist/ build/
|
||||
rm -rf .pytest_cache/ .mypy_cache/ .ruff_cache/
|
||||
rm -rf htmlcov/ .coverage
|
||||
find . -type d -name "__pycache__" -exec rm -rf {} + 2>/dev/null || true
|
||||
find . -type f -name "*.pyc" -delete 2>/dev/null || true
|
||||
.PHONY: t
|
||||
t: gen-build-info ## Run one test path with tee'd output. Usage: make t T=test/path/to/spec.dart
|
||||
@mkdir -p test/.test-output
|
||||
@if [ -z "$(T)" ]; then echo "usage: make t T=test/path/to/spec.dart" >&2; exit 2; fi
|
||||
flutter test $(T) 2>&1 | tee test/.test-output/last.log
|
||||
|
||||
# CI targets (for GitHub Actions)
|
||||
ci-lint:
|
||||
pip install ruff mypy
|
||||
ruff check clide/ tests/
|
||||
mypy clide/
|
||||
.PHONY: test
|
||||
test: gen-build-info ## Fast dev loop: analyze + format + unit + widget + golden, NO coverage, parallel (~20s).
|
||||
ci/test.sh
|
||||
|
||||
ci-test:
|
||||
pip install -e ".[dev]"
|
||||
pytest --cov=clide/clide --cov-report=xml
|
||||
.PHONY: test-coverage
|
||||
test-coverage: gen-build-info ## Same suite WITH coverage → coverage/lcov.info (for the gate / CI). Slower.
|
||||
ci/test.sh --coverage
|
||||
|
||||
ci-build:
|
||||
pip install -e ".[build]"
|
||||
pyinstaller clide.spec --clean
|
||||
.PHONY: test-core
|
||||
test-core: gen-build-info ## Core subsystem tests (IPC, PTY, git, pane registry).
|
||||
ci/test_core.sh
|
||||
|
||||
# Distribution builds
|
||||
build-macos:
|
||||
@printf "$(BLUE)Building macOS distribution...$(RESET)\n"
|
||||
./scripts/build-macos.sh $(VERSION)
|
||||
.PHONY: test-a11y
|
||||
test-a11y: gen-build-info ## A11y contract (semantic coverage + keyboard + contrast + i18n).
|
||||
ci/test_a11y.sh
|
||||
|
||||
build-linux:
|
||||
@printf "$(BLUE)Building Linux AppImage...$(RESET)\n"
|
||||
./scripts/build-linux.sh $(VERSION)
|
||||
.PHONY: test-integration
|
||||
test-integration: gen-build-info ## Integration tests (real app boot; xvfb on headless Linux).
|
||||
ci/test_integration.sh
|
||||
|
||||
build-windows:
|
||||
@echo "$(BLUE)Building Windows installer...$(RESET)"
|
||||
powershell -ExecutionPolicy Bypass -File scripts/build-windows.ps1 -Version $(VERSION)
|
||||
.PHONY: test-e2e
|
||||
test-e2e: ## End-to-end Playwright smoke.
|
||||
ci/test_e2e.sh
|
||||
|
||||
build-all: build-linux
|
||||
@printf "\n"
|
||||
@printf "$(GREEN)Linux build complete.$(RESET)\n"
|
||||
@printf "$(BLUE)Note:$(RESET) macOS build requires: make build-macos VERSION=$(VERSION)\n"
|
||||
@printf "$(BLUE)Note:$(RESET) Windows build requires Windows: make build-windows VERSION=$(VERSION)\n"
|
||||
.PHONY: test-all
|
||||
test-all: test-core test test-a11y test-integration test-e2e ## Everything, sequentially.
|
||||
|
||||
.PHONY: coverage-gate
|
||||
coverage-gate: ## Coverage gate — fails if total line % < pubspec.yaml `coverage_floor:` (D-66). Assumes `make test-coverage` ran first.
|
||||
ci/coverage_gate.sh
|
||||
|
||||
.PHONY: changelog-gate
|
||||
changelog-gate: ## Changelog concision gate — fails on `## [Unreleased]` bullets over 60 words.
|
||||
ci/changelog_gate.sh
|
||||
|
||||
.PHONY: smoke-bundle
|
||||
smoke-bundle: gen-build-info ## Build Linux release bundle and run it under xvfb for 5s.
|
||||
ci/smoke_bundle.sh
|
||||
|
||||
# -- web UI harness ------------------------------------------------------
|
||||
|
||||
.PHONY: ui-dev
|
||||
ui-dev: ## Build web WASM + start localhost:4280 in the background.
|
||||
tools/ui/build.sh
|
||||
tools/ui/serve.sh
|
||||
|
||||
.PHONY: ui-stop
|
||||
ui-stop: ## Stop the background web server.
|
||||
tools/ui/stop.sh
|
||||
|
||||
.PHONY: ui-smoke
|
||||
ui-smoke: ## Build + serve + run Playwright smoke + stop.
|
||||
tools/ui/build.sh
|
||||
tools/ui/serve.sh
|
||||
@sh -c 'trap "tools/ui/stop.sh >/dev/null 2>&1" EXIT; cd tools/ui && npx playwright test smoke.spec.ts'
|
||||
|
||||
.PHONY: build
|
||||
build: gen-build-info clide-cli ## flutter build for the current OS (incl. the C CLI client).
|
||||
flutter build $(FLUTTER_OS)
|
||||
@install -m 755 $(CLIDE_CLI_BIN) $(CLI_BUNDLE_DEST)
|
||||
@echo "==> bundled C client at $(CLI_BUNDLE_DEST)"
|
||||
|
||||
.PHONY: build-linux
|
||||
build-linux: gen-build-info ## flutter build linux (desktop bundle).
|
||||
flutter build linux
|
||||
|
||||
.PHONY: build-macos
|
||||
build-macos: gen-build-info ## flutter build macos (desktop bundle).
|
||||
flutter build macos
|
||||
|
||||
.PHONY: build-windows
|
||||
build-windows: gen-build-info ## flutter build windows (desktop bundle).
|
||||
flutter build windows
|
||||
|
||||
# -- install / uninstall -----------------------------------------------------
|
||||
|
||||
# Install prefix. Bundle lands at $(INSTALL_PREFIX)/clide/ with a
|
||||
# symlink at $(INSTALL_DIR)/clide pointing into it.
|
||||
INSTALL_PREFIX ?= $(HOME)/.local/lib
|
||||
|
||||
ifeq ($(FLUTTER_OS),linux)
|
||||
BUNDLE_DIR := build/linux/x64/release/bundle
|
||||
# The C client ships beside the GUI runner so the in-app "Install clide
|
||||
# command in PATH" affordance (T-212) can self-install from the bundle.
|
||||
CLI_BUNDLE_DEST := $(BUNDLE_DIR)/clide-cli
|
||||
else ifeq ($(FLUTTER_OS),macos)
|
||||
BUNDLE_DIR := build/macos/Build/Products/Release/clide.app
|
||||
CLI_BUNDLE_DEST := $(BUNDLE_DIR)/Contents/MacOS/clide-cli
|
||||
else ifeq ($(FLUTTER_OS),windows)
|
||||
BUNDLE_DIR := build/windows/x64/runner/Release
|
||||
CLI_BUNDLE_DEST := $(BUNDLE_DIR)/clide-cli.exe
|
||||
endif
|
||||
|
||||
ICON_SIZES := 16 32 48 128 192 256 512
|
||||
|
||||
.PHONY: install
|
||||
install: build clide-cli ## Build + install clide to ~/.local (INSTALL_PREFIX, INSTALL_DIR).
|
||||
ifeq ($(FLUTTER_OS),linux)
|
||||
@mkdir -p $(INSTALL_PREFIX) $(INSTALL_DIR)
|
||||
rm -rf $(INSTALL_PREFIX)/clide
|
||||
cp -a $(BUNDLE_DIR) $(INSTALL_PREFIX)/clide
|
||||
install -m 755 $(CLIDE_CLI_BIN) $(INSTALL_DIR)/clide
|
||||
@for size in $(ICON_SIZES); do \
|
||||
dir=$(HOME)/.local/share/icons/hicolor/$${size}x$${size}/apps; \
|
||||
mkdir -p $$dir; \
|
||||
cp assets/logo/appicon-$${size}.png $$dir/clide.png; \
|
||||
done
|
||||
@mkdir -p $(HOME)/.local/share/applications
|
||||
@sed 's|Exec=clide|Exec=$(INSTALL_PREFIX)/clide/clide|' linux/clide.desktop \
|
||||
> $(HOME)/.local/share/applications/net.schweitz.clide.desktop
|
||||
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
|
||||
@update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true
|
||||
@echo "cli: $(INSTALL_DIR)/clide (C client)"
|
||||
@echo "gui: $(INSTALL_PREFIX)/clide/clide"
|
||||
@echo "desktop: ~/.local/share/applications/net.schweitz.clide.desktop"
|
||||
@echo "version: $(VERSION)"
|
||||
else ifeq ($(FLUTTER_OS),macos)
|
||||
@mkdir -p $(HOME)/Applications $(INSTALL_DIR)
|
||||
rm -rf $(HOME)/Applications/clide.app
|
||||
cp -a $(BUNDLE_DIR) $(HOME)/Applications/clide.app
|
||||
install -m 755 $(CLIDE_CLI_BIN) $(INSTALL_DIR)/clide
|
||||
@echo "gui: ~/Applications/clide.app"
|
||||
@echo "cli: $(INSTALL_DIR)/clide (C client)"
|
||||
@echo "version: $(VERSION)"
|
||||
else
|
||||
@echo "install not yet supported on $(FLUTTER_OS)"
|
||||
@exit 1
|
||||
endif
|
||||
|
||||
.PHONY: uninstall
|
||||
uninstall: ## Remove installed clide.
|
||||
ifeq ($(FLUTTER_OS),linux)
|
||||
rm -f $(INSTALL_DIR)/clide
|
||||
rm -rf $(INSTALL_PREFIX)/clide
|
||||
rm -f $(HOME)/.local/share/applications/net.schweitz.clide.desktop
|
||||
rm -f $(HOME)/.local/share/applications/clide.desktop
|
||||
@for size in $(ICON_SIZES); do \
|
||||
rm -f $(HOME)/.local/share/icons/hicolor/$${size}x$${size}/apps/clide.png; \
|
||||
done
|
||||
@gtk-update-icon-cache -f -t $(HOME)/.local/share/icons/hicolor 2>/dev/null || true
|
||||
@update-desktop-database $(HOME)/.local/share/applications 2>/dev/null || true
|
||||
@echo "uninstalled"
|
||||
else ifeq ($(FLUTTER_OS),macos)
|
||||
rm -f $(INSTALL_DIR)/clide
|
||||
rm -rf $(HOME)/Applications/clide.app
|
||||
@echo "uninstalled"
|
||||
endif
|
||||
|
||||
# -- dugite-native (bundled git) ------------------------------------------
|
||||
|
||||
DUGITE_VERSION := v2.53.0-3
|
||||
DUGITE_COMMIT := f49d009
|
||||
|
||||
ifeq ($(FLUTTER_OS),macos)
|
||||
ifeq ($(shell uname -m),arm64)
|
||||
DUGITE_PLATFORM := macOS-arm64
|
||||
else
|
||||
DUGITE_PLATFORM := macOS-x64
|
||||
endif
|
||||
else ifeq ($(FLUTTER_OS),linux)
|
||||
DUGITE_PLATFORM := ubuntu-x64
|
||||
else
|
||||
DUGITE_PLATFORM := windows-x64
|
||||
endif
|
||||
|
||||
DUGITE_TAR := dugite-native-v2.53.0-$(DUGITE_COMMIT)-$(DUGITE_PLATFORM).tar.gz
|
||||
DUGITE_URL := https://github.com/desktop/dugite-native/releases/download/$(DUGITE_VERSION)/$(DUGITE_TAR)
|
||||
DUGITE_DIR := native/dugite
|
||||
|
||||
.PHONY: dugite-fetch
|
||||
dugite-fetch: ## Download and extract the dugite-native git distribution.
|
||||
@if [ -f $(DUGITE_DIR)/bin/git ]; then echo "dugite already present at $(DUGITE_DIR)/bin/git"; exit 0; fi
|
||||
@mkdir -p $(DUGITE_DIR)
|
||||
curl -sL "$(DUGITE_URL)" | tar xz -C $(DUGITE_DIR)
|
||||
@echo "dugite-native $(DUGITE_VERSION) ($(DUGITE_PLATFORM)) extracted to $(DUGITE_DIR)/"
|
||||
|
||||
.PHONY: dugite-clean
|
||||
dugite-clean: ## Remove the dugite-native directory.
|
||||
rm -rf $(DUGITE_DIR)
|
||||
|
||||
# -- clide-cli ----------------------------------------------------------
|
||||
# The C `clide` shell client that talks to the in-process IPC server
|
||||
# (T-99 / T-126). One source file, no third-party deps; the build
|
||||
# target picks up whatever `cc` is on PATH.
|
||||
|
||||
CLIDE_CLI_SRC := native/clide-cli/clide.c
|
||||
ifeq ($(FLUTTER_OS),windows)
|
||||
CLIDE_CLI_BIN := native/windows-x64/clide.exe
|
||||
else
|
||||
CLIDE_CLI_BIN := native/$(if $(filter Darwin,$(shell uname -s)),macos,linux)-$(shell uname -m | sed 's/x86_64/x64/;s/aarch64/arm64/')/clide
|
||||
endif
|
||||
CC ?= cc
|
||||
|
||||
.PHONY: clide-cli
|
||||
clide-cli: $(CLIDE_CLI_BIN) ## Compile the C `clide` shell client.
|
||||
|
||||
$(CLIDE_CLI_BIN): $(CLIDE_CLI_SRC)
|
||||
@mkdir -p $(dir $(CLIDE_CLI_BIN))
|
||||
ifeq ($(FLUTTER_OS),windows)
|
||||
ci/build_cli_windows.sh
|
||||
else
|
||||
$(CC) -std=c99 -O2 -Wall -Wextra -o $(CLIDE_CLI_BIN) $(CLIDE_CLI_SRC)
|
||||
endif
|
||||
@echo "==> built $(CLIDE_CLI_BIN)"
|
||||
|
||||
.PHONY: clide-cli-clean
|
||||
clide-cli-clean: ## Remove the compiled C `clide` client.
|
||||
rm -f $(CLIDE_CLI_BIN)
|
||||
|
||||
# -- security -------------------------------------------------------------
|
||||
|
||||
.PHONY: security
|
||||
security: ## Supply-chain gate — osv-scanner over pubspec.lock (CI PR-merge pipeline; run locally on demand). Fails on a known advisory.
|
||||
ci/osv_scan.sh
|
||||
|
||||
# -- pre-push gate --------------------------------------------------------
|
||||
|
||||
.PHONY: decisions-validate
|
||||
decisions-validate: ## Parser dry-run over governance/{decisions,questions,rejected}/*.md.
|
||||
pql decisions validate
|
||||
|
||||
.PHONY: push-check
|
||||
# NOTE: the `security` (osv-scanner) gate is deliberately NOT in push-check —
|
||||
# it runs in the CI PR-merge pipeline (where the scanner is provisioned) so we
|
||||
# don't force every dev machine to install osv-scanner. Run it locally any time
|
||||
# with `make security`.
|
||||
push-check: decisions-validate changelog-gate test-coverage coverage-gate test-core ## Pre-push gate (fast — <2 min target). Order is fail-fast: instant gates (decisions, changelog) first, then the coverage suite + gate (the expensive, most-likely-to-fail stage) BEFORE test-core — a coverage miss aborts here instead of after running everything, so a fix doesn't force a full re-run of the rest. test-coverage already runs the a11y suite (test/a11y), so no separate test-a11y pass.
|
||||
|
||||
.PHONY: push-check-full
|
||||
push-check-full: push-check test-integration smoke-bundle ## Pre-release gate (push-check + integration + smoke; slower).
|
||||
|
||||
.PHONY: hooks
|
||||
hooks: ## Install the repo's git hooks.
|
||||
git config core.hooksPath .githooks
|
||||
@echo "hooks installed (core.hooksPath = .githooks)"
|
||||
|
||||
# -- housekeeping ---------------------------------------------------------
|
||||
|
||||
.PHONY: clean
|
||||
clean: ## Remove build artefacts.
|
||||
rm -rf build .dart_tool
|
||||
|
||||
.DEFAULT_GOAL := help
|
||||
|
||||
@@ -0,0 +1,227 @@
|
||||
# clide Policy
|
||||
|
||||
This document governs what clide is allowed to do at runtime, what it's allowed to depend on, and how contributors — human and agent — introduce code into the project. It is binding on all contributors. When in doubt, stop and ask.
|
||||
|
||||
Rationale for specific architectural choices referenced here and in code comments (the D-### markers) lives in `decisions/`. This document sets the rules; `decisions/` records why the rules produced the code they did in a given case. If the two ever disagree, the rule in this document wins until the document itself is changed.
|
||||
|
||||
## Why this document exists
|
||||
|
||||
clide's security model rests on a simple claim: the behavior of the app on a user's machine is fully determined by the signed release artifact and the files in this repository at the time of that release. Every file the app reads, every binary it executes, and every wasm component it instantiates on the default launch path was present, reviewed, and pinned when we built the release.
|
||||
|
||||
The moment that claim stops being true — the moment the app fetches something from the network that wasn't audited at build time — the entire sandboxing and trust story collapses. It doesn't matter how good the capability model around wasm components is if the components themselves are downloaded from the internet at first launch.
|
||||
|
||||
This policy exists to keep that claim true.
|
||||
|
||||
## The core rule: no network on the default launch path
|
||||
|
||||
clide does not perform network I/O during app startup, library initialization, or first use of any API, unless the user has explicitly asked for it.
|
||||
|
||||
"Explicitly asked for it" means the user pressed a button, ran a command, or otherwise took an action whose stated purpose is to cause a network fetch. Opening the app is not such an action. Opening a file is not such an action. Typing in a buffer is not such an action.
|
||||
|
||||
### What this prohibits
|
||||
|
||||
- Libraries that download their own native binaries on first import or first call (the `wasm_run` pattern).
|
||||
- Language servers, formatters, or grammars that auto-install from the network when a file of that language is opened.
|
||||
- Fonts, icons, or assets fetched from a CDN rather than bundled.
|
||||
- Telemetry, crash reporting, or analytics that connect on startup regardless of user consent.
|
||||
- Auto-update checks that run without the user asking.
|
||||
- Any transitive dependency behavior that does the above, regardless of whether our code triggered it.
|
||||
|
||||
### What this allows, grudgingly
|
||||
|
||||
Signed, pinned fetches are permitted for a narrow set of cases where the alternative is materially worse. To qualify, all of the following must be true:
|
||||
|
||||
1. The artifact URL is hardcoded in the repository, not constructed at runtime.
|
||||
2. The artifact is verified against a cryptographic hash committed to the repository, or a signature verified against a public key committed to the repository.
|
||||
3. The fetch is cached; subsequent launches do not re-fetch.
|
||||
4. Fetch failure produces a clear error, not a silent degradation.
|
||||
5. The first launch after install can complete its primary function without the fetch succeeding. If the fetch is required for the app to work at all, this is not a "grudging allowance" case — vendor it instead.
|
||||
|
||||
If you cannot satisfy all five, the answer is vendoring or explicit user action, not a signed fetch.
|
||||
|
||||
### What this allows freely
|
||||
|
||||
Network I/O is unrestricted when the user has explicitly initiated it. Examples:
|
||||
|
||||
- "Install the Rust grammar" — fetches on button press, fine.
|
||||
- Cloning a repository the user typed the URL of.
|
||||
- Connecting to an LSP server the user configured.
|
||||
- Any feature whose stated purpose is to talk to the network (HTTP client, git, SSH).
|
||||
|
||||
The rule is about the *default* path, not about forbidding network ever.
|
||||
|
||||
## Dependencies: zero by design
|
||||
|
||||
clide's default answer to "should we add a dependency" is **no**. Every dependency is a liability — code we didn't write, running in our users' address space, that we are nonetheless responsible for. The right number of dependencies is the number that makes clide meaningfully better than writing the code ourselves, and that number is small.
|
||||
|
||||
This is not a preference. It is the project's architectural stance, documented in `CLAUDE.md` as "no dep graph by design, exact-pinned, CVE-checked, audited." Every dependency that exists in clide is there because someone argued for it, someone reviewed the argument, and the answer was yes. Dependencies do not accumulate by accident.
|
||||
|
||||
### What "no dep graph by design" means in practice
|
||||
|
||||
- **Prefer writing it.** A 100-line utility written in-repo is cheaper than a 100-line dependency, even though the dependency looks free. We own the code either way; the dependency just hides that fact behind a pub.dev page.
|
||||
- **Prefer the standard library.** Dart, Flutter, and Rust all have large standard libraries. Reach for them before reaching for a package. "There's a package that does this" is not a reason; "the standard library cannot do this reasonably" is.
|
||||
- **Prefer inlining over depending.** If a package is small and does one thing we need, copy the relevant code (with attribution) rather than taking on the dependency. License permitting, this is usually the right call for utilities under a few hundred lines.
|
||||
- **Prefer vendoring over fetching.** When a dependency is unavoidable and small enough to own, fork it into the repo and maintain it ourselves. The dep graph stops at our fence.
|
||||
- **Reject deep trees.** A direct dependency that itself has fifteen transitive dependencies is fifteen dependencies we are taking on, not one. Evaluate the whole subtree, not just the top.
|
||||
- **Format engines clear the bar.** Packages that parse or render external file formats (SVG, markdown, HTML, terminal escapes, tree-sitter grammars) are adoptable — they are not shortcuts for lazy coding but maintained renderers for specs we didn't write. The distinction: UI chrome (panels, tabs, canvas, layout) we own; someone else's file format we adopt and sandbox. See D-58.
|
||||
|
||||
### Every dependency is audited
|
||||
|
||||
For every dependency that does land, the following must be true and documented:
|
||||
|
||||
- **Exact-pinned.** `1.2.3`, never `^1.2.3` or `>=1.2.0 <2.0.0`. Lockfiles are committed. Updates are deliberate, reviewed PRs, not automatic.
|
||||
- **CVE-checked.** The dependency is checked against known vulnerability databases at the time of adoption and on every subsequent CI run via the pipeline's automated CVE gate. A dependency with unpatched high-severity CVEs does not land; a dependency that develops an unpatched high-severity CVE after it lands becomes a release blocker until resolved. The CVE check is part of the build, not a periodic chore — if CI is green, the dep tree was clean at build time.
|
||||
- **Source-reviewed.** Someone on the project has read the source of the dependency, or of the relevant subset, before it lands. "I trust this maintainer" is not a substitute. For large deps, the review can be scoped to the parts we actually use.
|
||||
- **Recorded.** The reason for adoption, the reviewer, the source SHA or version, and the alternatives considered are captured in the PR that adds it.
|
||||
- **Justified in place.** Every dependency in `pubspec.yaml` (and equivalent manifests) carries an inline comment explaining what it does, why we took it rather than writing the code, and its license. PR descriptions are not a substitute — the justification must survive in the manifest where future contributors will actually look. If a dependency can't be justified in two or three lines next to its version pin, it probably shouldn't land.
|
||||
|
||||
### The "is this worth it" test
|
||||
|
||||
Before adding any dependency, answer honestly:
|
||||
|
||||
1. What specifically does this do that we cannot reasonably do ourselves?
|
||||
2. How much code would we write if we did it ourselves? (If the answer is under ~200 lines, strongly prefer writing it.)
|
||||
3. What is the full transitive dep tree we are accepting?
|
||||
4. Who maintains it, and what happens to clide if they stop?
|
||||
5. Is the dependency's scope larger than our need? (Most packages are — we take on all the code, not just the parts we use.)
|
||||
|
||||
A dependency that passes this test and the vetting checklist below is welcome. A dependency that passes neither is a no. A dependency that passes one but not the other is a conversation, not a commit.
|
||||
|
||||
## Dependency vetting checklist
|
||||
|
||||
Before adding any new dependency (direct or transitive) to clide, verify:
|
||||
|
||||
**Network behavior**
|
||||
- Does the package perform network I/O during import, initialization, or first call? If yes, reject or vendor.
|
||||
- Does the package have a `postinstall`, `setup`, or equivalent script that downloads binaries? If yes, reject or vendor.
|
||||
- Does any transitive dependency do either of the above? `flutter pub deps` and read the code of anything unfamiliar.
|
||||
|
||||
**Binary provenance**
|
||||
- If the package ships native binaries, are they built from source in the same repo, or fetched from a release artifact? If fetched, treat as a network-at-launch violation.
|
||||
- If the package expects to download binaries on a different machine (CI, developer setup), that is also disqualifying for runtime deps.
|
||||
|
||||
**Maintainership**
|
||||
- Single-maintainer packages require explicit sign-off and a documented fallback plan. Note the maintainer and last meaningful commit in the PR.
|
||||
- Packages with no activity in 12+ months require either a fork we control or a decision to own the code ourselves.
|
||||
|
||||
**Surface area**
|
||||
- Prefer packages that do one thing. A dependency that adds 15 transitive deps to solve a 100-line problem should be inlined instead.
|
||||
|
||||
**Version pinning, lockfiles, and CVE review**
|
||||
- See "Dependencies: zero by design" above. All the requirements there apply: exact-pinned, CVE-checked, source-reviewed, justified in place, recorded in the PR.
|
||||
- `pubspec.lock` (and equivalent lockfiles for other toolchains) is committed to the repository and regenerated on every dependency change. A stale lockfile is a supply-chain hazard: a dependency removed from `pubspec.yaml` that persists in `pubspec.lock` will continue to install in CI and on fresh clones. Lockfile regeneration is part of the dep-change PR, not a follow-up.
|
||||
|
||||
**License**
|
||||
- See License and attribution rules below. A dependency with an incompatible or unclear license is disqualifying regardless of technical merit.
|
||||
|
||||
If you can't answer any of these questions, you haven't vetted the dependency yet.
|
||||
|
||||
## Removing a dependency
|
||||
|
||||
Removal is not the reverse of addition. Adding a dependency involves a single decision and a single commit; removing one involves finding every place the dependency or its transitive siblings have leaked into the codebase and making sure none of them outlive the removal PR.
|
||||
|
||||
When removing a dependency:
|
||||
|
||||
1. **Grep the entire repository** for references to the package, its exports, and any type names it contributed. `rg '<package>|<PackageType>|<prefix_>'` across the repo. Zero hits outside git history is the goal. A single lingering import will break the build; a single lingering FFI stub or type alias will compile fine and fail at runtime.
|
||||
2. **Regenerate the lockfile** as part of the same PR. A `pubspec.yaml` with the dep removed but a `pubspec.lock` that still pins it is a partial removal, and CI or a fresh clone will happily continue installing the package.
|
||||
3. **Update `app/assets/licenses.yaml`** to drop the removed package and any transitive deps it brought in that aren't pulled by anything else. If the license manifest is auto-generated on release, verify the generation script sees the change; if it's maintained by hand, edit it in the same PR.
|
||||
4. **Remove any vendored artifacts** tied to the dep — binaries, prebuilt assets, generated bindings — and delete their `BUILD.md` records. An orphaned vendored binary is worse than a removed one because it looks legitimate.
|
||||
5. **Check for architectural assumptions** that the dep was carrying. If the removed package was the thing that justified a specific data flow, build step, or platform strategy, either the replacement picks up those responsibilities or the architecture has actually changed and the relevant design decision (see `decisions/`) needs updating.
|
||||
|
||||
A dependency is not removed until all five are true. "I deleted the line from pubspec.yaml" is the start of the removal, not the end.
|
||||
|
||||
## Vendored binary rebuild process
|
||||
|
||||
clide ships pre-built native libraries (currently `libtree-sitter.so` and per-platform equivalents) vendored into the repository. These binaries are part of the trust boundary: the signed release contains exactly these bytes, and users running clide are running exactly this code.
|
||||
|
||||
Every vendored binary has a corresponding `BUILD.md` (or equivalent) next to it in the repo that records:
|
||||
|
||||
1. The exact upstream source (git URL and commit SHA, not a version tag).
|
||||
2. The full build command, including all compile flags.
|
||||
3. The toolchain version used (compiler, linker, target triple).
|
||||
4. The expected output size and a SHA-256 hash of the resulting binary.
|
||||
5. Any patches applied to the source, stored as `.patch` files in the same directory.
|
||||
|
||||
Rebuilds are deliberate events, not background maintenance:
|
||||
|
||||
- Rebuilds happen in CI, not on a contributor's laptop. A matrix job cross-compiles for all supported platforms and produces the full set of binaries from a single source SHA.
|
||||
- The rebuild PR updates `BUILD.md`, the binaries, and the hashes in one atomic change. The hash change is the signal in review that binary content changed and must be re-reviewed.
|
||||
- No binary is committed without a corresponding source SHA and build log. If you can't reproduce the binary from the recorded inputs, the binary is not trustworthy and must be rebuilt.
|
||||
- Security patches to vendored dependencies are tracked with the same urgency as source-level vulnerabilities. "We're on an old libtree-sitter because rebuilding is annoying" is not acceptable.
|
||||
|
||||
Supported platforms and their binary artifacts are documented in the repository root. Dropping a platform requires a policy decision, not a build convenience. Adding a platform requires adding it to the CI matrix and rebuilding all vendored binaries for it before the platform ships.
|
||||
|
||||
## Telemetry and phone-home rules
|
||||
|
||||
clide does not phone home. The app does not report to any server — ours or anyone else's. This is not a "default off" setting; it is an architectural commitment. clide is a space to think, not a surface for data collection.
|
||||
|
||||
This means:
|
||||
|
||||
- No analytics SDKs. Not Firebase, not Sentry, not Mixpanel, not a hand-rolled metrics endpoint. Not in debug builds, not in release builds, not behind a feature flag.
|
||||
- No crash reporters that upload automatically. Crashes produce local logs the user can read and, if they choose, attach to a bug report they submit manually.
|
||||
- No "check for updates" that runs without the user asking. Update checks are a button.
|
||||
- No license validation that calls home. clide is MIT; there is nothing to validate.
|
||||
- No feature flags fetched from a server. Configuration is local.
|
||||
- No A/B testing, no experiments, no remote config, no "anonymous usage statistics."
|
||||
|
||||
If a future contributor proposes telemetry under any framing — opt-in, anonymized, debug-only, "just errors," "just for us," "just to improve the product" — the answer is no. The architectural commitment is the feature. Users installing clide are choosing a tool that does not watch them, and that promise is worth more than any data we could collect.
|
||||
|
||||
This rule does not bend. Proposals to add telemetry are out of scope for this project, full stop. If you want telemetry in your editor, clide is not the editor for you, and that's fine — fork it.
|
||||
|
||||
## License and attribution rules
|
||||
|
||||
clide itself is MIT-licensed. Every dependency, vendored binary, bundled font, and bundled asset must be:
|
||||
|
||||
1. Compatible with clide's license.
|
||||
2. Attributed in the about screen's license manifest.
|
||||
3. Attributed correctly — the license text and copyright notice must match upstream, not a paraphrase.
|
||||
|
||||
### Compatibility matrix
|
||||
|
||||
Compatible with clide (permissive): MIT, Apache-2.0, BSD-2/3, ISC, Zlib, Unlicense, CC0.
|
||||
|
||||
Compatible with care (copyleft): MPL-2.0 is acceptable for libraries; LGPL is acceptable only for dynamically-linked vendored binaries where we can demonstrate users can replace the library.
|
||||
|
||||
Not compatible: GPL (any version) for linked code, AGPL for anything, SSPL, "commercial use prohibited" licenses, custom licenses not reviewed by someone qualified to review them.
|
||||
|
||||
When in doubt about a license, the dependency does not land until the question is resolved.
|
||||
|
||||
### Attribution requirements
|
||||
|
||||
- The license manifest at `app/assets/licenses.yaml` lists every dependency with its license, copyright notice, and upstream URL.
|
||||
- Transitive dependencies are listed, not just direct ones. If `wasm_run` pulls in `wasmtime` which pulls in `cranelift`, all three appear.
|
||||
- Apache-2.0 dependencies get their `NOTICE` file content preserved verbatim, not summarized.
|
||||
- Apache-2.0-with-LLVM-exception (e.g., Cranelift, parts of LLVM) requires the LLVM exception text specifically, not just the Apache-2.0 boilerplate.
|
||||
- Fonts and icon sets get attributed even if the license doesn't strictly require it. It's the right thing to do.
|
||||
- `app/assets/licenses.yaml` is regenerated as part of the release build, not maintained by hand. A release that ships a stale manifest is a release defect.
|
||||
|
||||
Adding a dependency means updating the license manifest in the same PR. No exceptions.
|
||||
|
||||
## Changelog and commit conventions
|
||||
|
||||
clide follows [Keep a Changelog 1.1](https://keepachangelog.com/en/1.1.0/) for `CHANGELOG.md` and [Conventional Commits 1.0](https://www.conventionalcommits.org/en/v1.0.0/) for commit messages. Enforcement is handled by the project's git skill; this section exists so human contributors know the standard before their first PR, and so the connection between these conventions and the rest of the policy is explicit.
|
||||
|
||||
Security-relevant changes — CVE responses, dependency-driven vulnerability fixes, the removal of a phoning-home transitive dep, anything where the rules in this document were the reason for the change — go under the `Security` heading of the release's changelog entry, regardless of whether the code change itself looks security-shaped. That heading is the trail future-us follows to reconstruct why a dep was bumped or removed. Lumping security fixes under `Fixed` because the diff looks like a normal bug fix loses that signal and is the wrong choice even when it's technically accurate.
|
||||
|
||||
## When this policy is wrong
|
||||
|
||||
This document is a set of rules, but rules encode judgment calls that may not survive contact with a specific situation. If you think the policy is wrong for a specific case, the process is:
|
||||
|
||||
1. Stop. Do not ship the thing.
|
||||
2. Write down the specific case and why you think the rule fails.
|
||||
3. Propose a change to this document.
|
||||
4. Get the change reviewed on its merits.
|
||||
5. If the change lands, proceed. If not, the rule holds.
|
||||
|
||||
"The rule is inconvenient for this task" is not an argument for the rule being wrong. "The rule produces a worse outcome than the alternative, measured against the reasons the rule exists" is.
|
||||
|
||||
## Notes for agents
|
||||
|
||||
If you are an AI agent working on this repository, the following guidance is binding:
|
||||
|
||||
- The default answer to "should I add a dependency" is no. Write the code instead, or use the standard library. If you think a dependency is genuinely warranted, stop and make the case to the human before adding it — do not add it and justify it afterward.
|
||||
- Do not add dependencies without walking the vetting checklist. Record your answers in the PR description.
|
||||
- Do not add code that performs network I/O on the default launch path. If you're not sure whether a library does this, read its source before adding it, not after.
|
||||
- Do not vendor binaries without creating the corresponding `BUILD.md` and recording the source SHA and build command. A committed binary with no reproducibility record is worse than no binary at all.
|
||||
- Do not add telemetry, analytics, crash reporting, remote config, feature flags, or any other phone-home mechanism under any framing. This is not a "default off" setting; the project does not have telemetry and will not have telemetry. If you think you need it, you are wrong.
|
||||
- When in doubt, stop and ask the human. Pivoting architectures in a single session to route around a principle is almost always wrong; the principle is there for a reason that may not be visible from inside the task.
|
||||
@@ -1,168 +1,59 @@
|
||||
# Clide
|
||||
# clide
|
||||
|
||||
A terminal-based IDE that wraps Claude Code CLI, putting AI-assisted development at the center of your workflow.
|
||||
An IDE for Claude Code CLI. Native rendering, terminal-first interaction, pql-powered queries, canvas and graph surfaces. Linux and macOS.
|
||||
|
||||
## Why Clide?
|
||||
## Architecture
|
||||
|
||||
Claude Code is powerful, but switching between terminal, editor, and project tools breaks your flow. Clide brings everything into one interface:
|
||||
Single Flutter package at the repo root. The app hosts everything in-process: IPC server, subsystem handlers (pane, files, editor, git, pql), and the extension framework. tmux owns Claude session persistence (D-41).
|
||||
|
||||
- **Claude stays visible** — Claude Code runs in the center panel, always accessible
|
||||
- **Context at a glance** — File tree, git status, problems, and TODOs in dedicated panels
|
||||
- **Panels appear when needed** — Editor, diff viewer, and terminal stay hidden until you need them
|
||||
- **Git integration** — Commit, stash, pull, and push via Claude with built-in skills
|
||||
- **22 themes** — From summer-night to dracula, with custom theme support
|
||||
- **`lib/`** — all Dart code. Core subsystems (`lib/src/`), kernel services (`lib/kernel/`), UI widgets (`lib/widgets/`), built-in extensions (`lib/builtin/`), the extension framework (`lib/extension/`).
|
||||
- **PTY** — `lib/src/pty/` spawns child processes via Dart FFI `posix_openpt()` + `posix_spawn()` directly; no external helper binary.
|
||||
- **`native/`** — vendored native libraries (`libtree-sitter.so` with wasmtime embedded). Linux only today.
|
||||
- **[pql](https://github.com/postmeridiem/pql)** — external supporter tool. Clide wraps it for every query surface; never re-implements it.
|
||||
|
||||
## Screenshot
|
||||
Claude drives the UI through a `clide` CLI surface (Bash, not MCP). Every CLI subcommand has a UI affordance and every UI action has a CLI equivalent (D-6).
|
||||
|
||||
## Built-in extensions
|
||||
|
||||
canvas, claude, claude_control, decisions, diff, editor, extensions_ui, files, git, graph, grammars_core, ipc_status, keybindings_ui, markdown, pql, problems, settings_ui, terminal, theme_picker, tickets, todos, welcome.
|
||||
|
||||
## Building
|
||||
|
||||
Requires Flutter (stable channel) and [pql](https://github.com/postmeridiem/pql)
|
||||
on the host. pql is a hard dependency — the pre-push gate runs `pql decisions
|
||||
validate` and the governance + ticket workflow is built on it (see its repo for
|
||||
install). One-time setup:
|
||||
|
||||
```
|
||||
┌─────────────────┬─────────────────────────┬──────────────────┐
|
||||
│ Sidebar │ Workspace │ Context │
|
||||
│ │ [Editor][Diff][Terminal]│ │
|
||||
│ [Files][Git] │ (appears when needed) │ [Jira][TODOs] │
|
||||
│ [Tree] ├─────────────────────────┤ [Problems] │
|
||||
│ │ │ │
|
||||
│ │ Claude │ │
|
||||
│ │ (always visible) │ │
|
||||
│ │ │ │
|
||||
├─────────────────┤ ├──────────────────┤
|
||||
│ ⎇ main ▾ │ │ │
|
||||
│ staged: 2 │ │ │
|
||||
└─────────────────┴─────────────────────────┴──────────────────┘
|
||||
make hooks && flutter pub get
|
||||
pql init # once, in the repo root — wires up the pql skill + perms
|
||||
```
|
||||
|
||||
## Features
|
||||
Then:
|
||||
|
||||
### Left Sidebar
|
||||
- **Files** — Project file tree with syntax-aware icons
|
||||
- **Git** — Staged/unstaged changes with action buttons
|
||||
- **Tree** — Visual branch graph
|
||||
- **Branch status** — Current branch with quick switcher
|
||||
|
||||
### Center
|
||||
- **Claude Code** — Full PTY terminal integration, always visible
|
||||
- **Editor** — Syntax highlighting via tree-sitter
|
||||
- **Diff** — Side-by-side diff viewer
|
||||
- **Terminal** — Command execution
|
||||
|
||||
### Right Context
|
||||
- **Jira** — Issue display via CLI integration
|
||||
- **TODOs** — Code comments and TODO.md items
|
||||
- **Problems** — Linter errors and warnings
|
||||
|
||||
### Git Operations
|
||||
|
||||
Click buttons in the Git panel to delegate operations to Claude:
|
||||
|
||||
| Button | Skill | What Claude Does |
|
||||
|--------|-------|------------------|
|
||||
| Commit | `/commit` | Reviews changes, writes commit message |
|
||||
| Stash | `/stash` | Stashes working changes |
|
||||
| Pull | `/pull` | Pulls with rebase, helps resolve conflicts |
|
||||
| Push | `/push` | Pushes to remote, sets upstream if needed |
|
||||
|
||||
Skills are installed automatically to your project's `.claude/skills/` directory.
|
||||
|
||||
## Installation
|
||||
|
||||
### Requirements
|
||||
|
||||
- Python 3.12+
|
||||
- Git
|
||||
- Claude Code CLI (installed and authenticated)
|
||||
|
||||
### Setup
|
||||
|
||||
```bash
|
||||
git clone <repo-url>
|
||||
cd clide
|
||||
make setup
|
||||
make run
|
||||
```
|
||||
make run # launch the desktop app
|
||||
make test # fast suite: analyze + format + unit + widget + golden
|
||||
make test-core # core subsystem tests (IPC, PTY, git, pane registry)
|
||||
make test-a11y # accessibility contract tests
|
||||
make test-integration # real app boot integration tests
|
||||
make build-linux # flutter build linux
|
||||
make build-macos # flutter build macos
|
||||
make push-check # pre-push gate: decisions + core + fast + a11y + coverage + changelog
|
||||
```
|
||||
|
||||
Or install directly:
|
||||
## Status
|
||||
|
||||
```bash
|
||||
pip install -e .
|
||||
clide
|
||||
```
|
||||
|
||||
## Keybindings
|
||||
|
||||
All shortcuts use `Alt` to avoid conflicts with Claude Code input.
|
||||
|
||||
| Action | Binding |
|
||||
|--------|---------|
|
||||
| Toggle left sidebar | `Alt+B` |
|
||||
| Toggle right sidebar | `Alt+Shift+B` |
|
||||
| Toggle terminal | `` Alt+` `` |
|
||||
| Focus Claude | `Alt+1` |
|
||||
| Focus Editor | `Alt+2` |
|
||||
| Focus Terminal | `Alt+3` |
|
||||
| Toggle compact mode | `Alt+C` |
|
||||
| Select theme | `Alt+T` |
|
||||
| Quit | `Alt+Q` |
|
||||
|
||||
## Themes
|
||||
|
||||
22 built-in themes. Press `Alt+T` to switch.
|
||||
|
||||
| Category | Themes |
|
||||
|----------|--------|
|
||||
| Core | summer-night (default), summer-day |
|
||||
| Popular | one-dark, one-dark-pro, one-light, dracula, nord, gruvbox-dark, gruvbox-light |
|
||||
| Seasonal | winter-is-coming, monokai-winter, fall, dark-autumn |
|
||||
| Special | all-hallows-eve, halloween, christmas, santa-baby |
|
||||
| Hacker | pro-hacker, hacker-style |
|
||||
|
||||
Create custom themes in `~/.clide/themes/` as TOML files.
|
||||
|
||||
## Configuration
|
||||
|
||||
Settings stored in `~/.clide/settings.json`:
|
||||
|
||||
```json
|
||||
{
|
||||
"theme": "summer-night",
|
||||
"compact_mode": false,
|
||||
"jira_enabled": false
|
||||
}
|
||||
```
|
||||
|
||||
Override with environment variables:
|
||||
|
||||
```bash
|
||||
CLIDE_THEME=dracula clide
|
||||
```
|
||||
|
||||
## Tech Stack
|
||||
|
||||
| Component | Library |
|
||||
|-----------|---------|
|
||||
| Runtime | Python 3.12+ |
|
||||
| TUI Framework | Textual |
|
||||
| CLI | Typer |
|
||||
| Data Validation | Pydantic v2 |
|
||||
| Extensions | Pluggy |
|
||||
| Syntax Highlighting | tree-sitter |
|
||||
|
||||
## Development
|
||||
|
||||
```bash
|
||||
make setup # Create venv, install deps
|
||||
make run # Run application
|
||||
make test # Run all tests
|
||||
make typecheck # Run mypy
|
||||
make lint # Run ruff
|
||||
make format # Format code
|
||||
```
|
||||
Pre-v2.0 (`2.0.0-dev`). Interaction model and panel system landed. The Python Textual v1.2.0 predecessor is archived under [`legacy/`](https://github.com/postmeridiem/clide/tree/main/legacy).
|
||||
|
||||
## Documentation
|
||||
|
||||
- [User Manual](docs/user-manual.md) — How to use Clide
|
||||
- [UI/UX Specification](docs/tui-ide-spec.md) — Design decisions
|
||||
- [Architecture](docs/ARCHITECTURE.md) — Technical overview
|
||||
- [Code Organization](docs/code-organization.md) — Project structure
|
||||
- [`docs/architecture.md`](docs/architecture.md) — current architecture (read this first).
|
||||
- [`CONTRIBUTING.md`](CONTRIBUTING.md) — how to clone, build, test, file tickets, and write D-records.
|
||||
- [`docs/initial-plan.md`](docs/initial-plan.md) — historical design doc; preserved as a snapshot of the 2026-04 plan, much of it now superseded.
|
||||
- [`governance/decisions/`](governance/decisions/) — confirmed decisions (`D-NNN`), open questions (`Q-NNN`), rejected alternatives (`R-NNN`).
|
||||
- [`CLAUDE.md`](CLAUDE.md) — Claude-addressed working notes (guardrails, repo layout).
|
||||
|
||||
## License
|
||||
|
||||
MIT
|
||||
MIT. See [`LICENSE`](LICENSE).
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
include: package:flutter_lints/flutter.yaml
|
||||
|
||||
formatter:
|
||||
page_width: 160
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2026 Jeroen Schweitzer
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,35 @@
|
||||
# clide tmux.conf — loaded via tmux -f for every Claude pane session.
|
||||
# Tuned for embedding inside xterm.dart; no status bar, large
|
||||
# scrollback, mouse-scroll passthrough, zero escape delay.
|
||||
|
||||
# No status bar — clide renders its own pane chrome.
|
||||
set -g status off
|
||||
|
||||
# 50k lines of scrollback (tmux default is 2000).
|
||||
set -g history-limit 50000
|
||||
|
||||
# Zero escape delay — xterm.dart delivers escape sequences
|
||||
# atomically, so the 500ms default just adds latency.
|
||||
set -sg escape-time 0
|
||||
|
||||
# Mouse on — scroll wheel events reach tmux's copy-mode so the
|
||||
# user can scroll back through Claude output.
|
||||
set -g mouse on
|
||||
|
||||
# 256color + true-color passthrough.
|
||||
set -g default-terminal "xterm-256color"
|
||||
set -ga terminal-overrides ",xterm-256color:Tc"
|
||||
|
||||
# Don't ring the bell visually or audibly — clide owns notifications.
|
||||
set -g visual-bell off
|
||||
set -g bell-action none
|
||||
|
||||
# Keep the session alive when the shell exits — clide manages
|
||||
# lifecycle via pane.close, not tmux session destruction.
|
||||
set -g remain-on-exit off
|
||||
|
||||
# Allow alt-screen passthrough for full-screen programs.
|
||||
set -g alternate-screen on
|
||||
|
||||
# Focus events let the terminal's focus tracking work through tmux.
|
||||
set -g focus-events on
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2020 The JetBrains Mono Project Authors (https://github.com/JetBrains/JetBrainsMono)
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://scripts.sil.org/OFL
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,93 @@
|
||||
Copyright 2010 The Josefin Sans Project Authors (https://github.com/ThomasJockin/JosefinSansFont-master), with Reserved Font Name "Josefin Sans".
|
||||
|
||||
This Font Software is licensed under the SIL Open Font License, Version 1.1.
|
||||
This license is copied below, and is also available with a FAQ at:
|
||||
https://openfontlicense.org
|
||||
|
||||
|
||||
-----------------------------------------------------------
|
||||
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
|
||||
-----------------------------------------------------------
|
||||
|
||||
PREAMBLE
|
||||
The goals of the Open Font License (OFL) are to stimulate worldwide
|
||||
development of collaborative font projects, to support the font creation
|
||||
efforts of academic and linguistic communities, and to provide a free and
|
||||
open framework in which fonts may be shared and improved in partnership
|
||||
with others.
|
||||
|
||||
The OFL allows the licensed fonts to be used, studied, modified and
|
||||
redistributed freely as long as they are not sold by themselves. The
|
||||
fonts, including any derivative works, can be bundled, embedded,
|
||||
redistributed and/or sold with any software provided that any reserved
|
||||
names are not used by derivative works. The fonts and derivatives,
|
||||
however, cannot be released under any other type of license. The
|
||||
requirement for fonts to remain under this license does not apply
|
||||
to any document created using the fonts or their derivatives.
|
||||
|
||||
DEFINITIONS
|
||||
"Font Software" refers to the set of files released by the Copyright
|
||||
Holder(s) under this license and clearly marked as such. This may
|
||||
include source files, build scripts and documentation.
|
||||
|
||||
"Reserved Font Name" refers to any names specified as such after the
|
||||
copyright statement(s).
|
||||
|
||||
"Original Version" refers to the collection of Font Software components as
|
||||
distributed by the Copyright Holder(s).
|
||||
|
||||
"Modified Version" refers to any derivative made by adding to, deleting,
|
||||
or substituting -- in part or in whole -- any of the components of the
|
||||
Original Version, by changing formats or by porting the Font Software to a
|
||||
new environment.
|
||||
|
||||
"Author" refers to any designer, engineer, programmer, technical
|
||||
writer or other person who contributed to the Font Software.
|
||||
|
||||
PERMISSION & CONDITIONS
|
||||
Permission is hereby granted, free of charge, to any person obtaining
|
||||
a copy of the Font Software, to use, study, copy, merge, embed, modify,
|
||||
redistribute, and sell modified and unmodified copies of the Font
|
||||
Software, subject to the following conditions:
|
||||
|
||||
1) Neither the Font Software nor any of its individual components,
|
||||
in Original or Modified Versions, may be sold by itself.
|
||||
|
||||
2) Original or Modified Versions of the Font Software may be bundled,
|
||||
redistributed and/or sold with any software, provided that each copy
|
||||
contains the above copyright notice and this license. These can be
|
||||
included either as stand-alone text files, human-readable headers or
|
||||
in the appropriate machine-readable metadata fields within text or
|
||||
binary files as long as those fields can be easily viewed by the user.
|
||||
|
||||
3) No Modified Version of the Font Software may use the Reserved Font
|
||||
Name(s) unless explicit written permission is granted by the corresponding
|
||||
Copyright Holder. This restriction only applies to the primary font name as
|
||||
presented to the users.
|
||||
|
||||
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
|
||||
Software shall not be used to promote, endorse or advertise any
|
||||
Modified Version, except to acknowledge the contribution(s) of the
|
||||
Copyright Holder(s) and the Author(s) or with their explicit written
|
||||
permission.
|
||||
|
||||
5) The Font Software, modified or unmodified, in part or in whole,
|
||||
must be distributed entirely under this license, and must not be
|
||||
distributed under any other license. The requirement for fonts to
|
||||
remain under this license does not apply to any document created
|
||||
using the Font Software.
|
||||
|
||||
TERMINATION
|
||||
This license becomes null and void if any of the above conditions are
|
||||
not met.
|
||||
|
||||
DISCLAIMER
|
||||
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
|
||||
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
|
||||
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
|
||||
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
|
||||
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
|
||||
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
|
||||
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
|
||||
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
|
||||
OTHER DEALINGS IN THE FONT SOFTWARE.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2023 Phosphor Icons
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
File diff suppressed because it is too large
Load Diff
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Executable
BIN
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user