Compare commits
41 Commits
b168a58134
...
cli-imp
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
c8d609ab78 | ||
|
|
35c828ba73 | ||
|
|
ecbfef537a | ||
|
|
47eecce8e9 | ||
|
|
b29c382583 | ||
|
|
e26816333f | ||
|
|
f772de8aef | ||
|
|
dd4d867c6e | ||
|
|
ffd074499a | ||
|
|
125938fba6 | ||
|
|
cd25cf61ca | ||
|
|
d9c9f6e541 | ||
|
|
acc5e12e3d | ||
|
|
039ab1c2a3 | ||
|
|
d63d6f0b9c | ||
|
|
3a1307dcdc | ||
|
|
6ea3108a20 | ||
|
|
81647545e7 | ||
|
|
39a832688d | ||
|
|
06229ce98b | ||
|
|
8d18552298 | ||
|
|
f3788eb687 | ||
|
|
e9af529f6e | ||
|
|
70271c14d6 | ||
|
|
d9f99ef21d | ||
|
|
f5967a8e52 | ||
|
|
2c9de1a6c3 | ||
|
|
1161edb212 | ||
|
|
5ea976583e | ||
|
|
dcfd449b72 | ||
|
|
6b75697638 | ||
|
|
dc49f5209e | ||
|
|
7d40a81512 | ||
|
|
4185abe05d | ||
|
|
d54f669c5e | ||
|
|
45126f04a6 | ||
|
|
dfa44e5bcd | ||
|
|
53ef21d653 | ||
|
|
41504b4941 | ||
|
|
d36850f181 | ||
|
|
5ce18e0ebc |
232
.beads/.br_history/issues.20260212_161438.jsonl
Normal file
232
.beads/.br_history/issues.20260212_161438.jsonl
Normal file
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@@ -1 +1 @@
|
||||
bd-3qn6
|
||||
bd-xsgw
|
||||
|
||||
17
.claude/hooks/on-file-write.sh
Executable file
17
.claude/hooks/on-file-write.sh
Executable file
@@ -0,0 +1,17 @@
|
||||
#!/bin/bash
|
||||
# Ultimate Bug Scanner - Claude Code Hook
|
||||
# Runs on every file save for UBS-supported languages (JS/TS, Python, C/C++, Rust, Go, Java, Ruby)
|
||||
# Claude Code hooks receive context as JSON on stdin.
|
||||
|
||||
INPUT=$(cat)
|
||||
FILE_PATH=$(echo "$INPUT" | jq -r '.tool_input.file_path // empty')
|
||||
CWD=$(echo "$INPUT" | jq -r '.cwd // empty')
|
||||
|
||||
if [[ "$FILE_PATH" =~ \.(js|jsx|ts|tsx|mjs|cjs|py|pyw|pyi|c|cc|cpp|cxx|h|hh|hpp|hxx|rs|go|java|rb)$ ]]; then
|
||||
echo "🔬 Running bug scanner..."
|
||||
if ! command -v ubs >/dev/null 2>&1; then
|
||||
echo "⚠️ 'ubs' not found in PATH; install it before using this hook." >&2
|
||||
exit 0
|
||||
fi
|
||||
ubs "$FILE_PATH" --ci 2>&1 | head -50
|
||||
fi
|
||||
50
.cline/rules
Normal file
50
.cline/rules
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
````markdown
|
||||
## UBS Quick Reference for AI Agents
|
||||
|
||||
UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On**
|
||||
|
||||
**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh | bash`
|
||||
|
||||
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
ubs file.ts file2.py # Specific files (< 1s) — USE THIS
|
||||
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||
ubs --only=js,python src/ # Language filter (3-5x faster)
|
||||
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||
ubs --help # Full command reference
|
||||
ubs sessions --entries 1 # Tail the latest install session log
|
||||
ubs . # Whole project (ignores things like .venv and node_modules automatically)
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```
|
||||
⚠️ Category (N errors)
|
||||
file.ts:42:5 – Issue description
|
||||
💡 Suggested fix
|
||||
Exit code: 1
|
||||
```
|
||||
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||
|
||||
**Fix Workflow:**
|
||||
1. Read finding → category + fix suggestion
|
||||
2. Navigate `file:line:col` → view context
|
||||
3. Verify real issue (not false positive)
|
||||
4. Fix root cause (not symptom)
|
||||
5. Re-run `ubs <file>` → exit 0
|
||||
6. Commit
|
||||
|
||||
**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits.
|
||||
|
||||
**Bug Severity:**
|
||||
- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks
|
||||
- **Important** (production): Type narrowing, division-by-zero, resource leaks
|
||||
- **Contextual** (judgment): TODO/FIXME, console logs
|
||||
|
||||
**Anti-Patterns:**
|
||||
- ❌ Ignore findings → ✅ Investigate each
|
||||
- ❌ Full scan per edit → ✅ Scope to file
|
||||
- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`)
|
||||
````
|
||||
50
.codex/rules/ubs.md
Normal file
50
.codex/rules/ubs.md
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
````markdown
|
||||
## UBS Quick Reference for AI Agents
|
||||
|
||||
UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On**
|
||||
|
||||
**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh | bash`
|
||||
|
||||
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
ubs file.ts file2.py # Specific files (< 1s) — USE THIS
|
||||
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||
ubs --only=js,python src/ # Language filter (3-5x faster)
|
||||
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||
ubs --help # Full command reference
|
||||
ubs sessions --entries 1 # Tail the latest install session log
|
||||
ubs . # Whole project (ignores things like .venv and node_modules automatically)
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```
|
||||
⚠️ Category (N errors)
|
||||
file.ts:42:5 – Issue description
|
||||
💡 Suggested fix
|
||||
Exit code: 1
|
||||
```
|
||||
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||
|
||||
**Fix Workflow:**
|
||||
1. Read finding → category + fix suggestion
|
||||
2. Navigate `file:line:col` → view context
|
||||
3. Verify real issue (not false positive)
|
||||
4. Fix root cause (not symptom)
|
||||
5. Re-run `ubs <file>` → exit 0
|
||||
6. Commit
|
||||
|
||||
**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits.
|
||||
|
||||
**Bug Severity:**
|
||||
- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks
|
||||
- **Important** (production): Type narrowing, division-by-zero, resource leaks
|
||||
- **Contextual** (judgment): TODO/FIXME, console logs
|
||||
|
||||
**Anti-Patterns:**
|
||||
- ❌ Ignore findings → ✅ Investigate each
|
||||
- ❌ Full scan per edit → ✅ Scope to file
|
||||
- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`)
|
||||
````
|
||||
16
.continue/config.json
Normal file
16
.continue/config.json
Normal file
@@ -0,0 +1,16 @@
|
||||
{
|
||||
"customCommands": [
|
||||
{
|
||||
"name": "scan-bugs",
|
||||
"description": "Run Ultimate Bug Scanner on current project",
|
||||
"prompt": "Run 'ubs --fail-on-warning .' and fix any critical issues found before proceeding"
|
||||
}
|
||||
],
|
||||
"slashCommands": [
|
||||
{
|
||||
"name": "quality",
|
||||
"description": "Check code quality with UBS",
|
||||
"run": "ubs ."
|
||||
}
|
||||
]
|
||||
}
|
||||
50
.cursor/rules
Normal file
50
.cursor/rules
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
````markdown
|
||||
## UBS Quick Reference for AI Agents
|
||||
|
||||
UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On**
|
||||
|
||||
**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh | bash`
|
||||
|
||||
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
ubs file.ts file2.py # Specific files (< 1s) — USE THIS
|
||||
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||
ubs --only=js,python src/ # Language filter (3-5x faster)
|
||||
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||
ubs --help # Full command reference
|
||||
ubs sessions --entries 1 # Tail the latest install session log
|
||||
ubs . # Whole project (ignores things like .venv and node_modules automatically)
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```
|
||||
⚠️ Category (N errors)
|
||||
file.ts:42:5 – Issue description
|
||||
💡 Suggested fix
|
||||
Exit code: 1
|
||||
```
|
||||
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||
|
||||
**Fix Workflow:**
|
||||
1. Read finding → category + fix suggestion
|
||||
2. Navigate `file:line:col` → view context
|
||||
3. Verify real issue (not false positive)
|
||||
4. Fix root cause (not symptom)
|
||||
5. Re-run `ubs <file>` → exit 0
|
||||
6. Commit
|
||||
|
||||
**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits.
|
||||
|
||||
**Bug Severity:**
|
||||
- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks
|
||||
- **Important** (production): Type narrowing, division-by-zero, resource leaks
|
||||
- **Contextual** (judgment): TODO/FIXME, console logs
|
||||
|
||||
**Anti-Patterns:**
|
||||
- ❌ Ignore findings → ✅ Investigate each
|
||||
- ❌ Full scan per edit → ✅ Scope to file
|
||||
- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`)
|
||||
````
|
||||
50
.gemini/rules
Normal file
50
.gemini/rules
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
````markdown
|
||||
## UBS Quick Reference for AI Agents
|
||||
|
||||
UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On**
|
||||
|
||||
**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh | bash`
|
||||
|
||||
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
ubs file.ts file2.py # Specific files (< 1s) — USE THIS
|
||||
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||
ubs --only=js,python src/ # Language filter (3-5x faster)
|
||||
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||
ubs --help # Full command reference
|
||||
ubs sessions --entries 1 # Tail the latest install session log
|
||||
ubs . # Whole project (ignores things like .venv and node_modules automatically)
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```
|
||||
⚠️ Category (N errors)
|
||||
file.ts:42:5 – Issue description
|
||||
💡 Suggested fix
|
||||
Exit code: 1
|
||||
```
|
||||
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||
|
||||
**Fix Workflow:**
|
||||
1. Read finding → category + fix suggestion
|
||||
2. Navigate `file:line:col` → view context
|
||||
3. Verify real issue (not false positive)
|
||||
4. Fix root cause (not symptom)
|
||||
5. Re-run `ubs <file>` → exit 0
|
||||
6. Commit
|
||||
|
||||
**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits.
|
||||
|
||||
**Bug Severity:**
|
||||
- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks
|
||||
- **Important** (production): Type narrowing, division-by-zero, resource leaks
|
||||
- **Contextual** (judgment): TODO/FIXME, console logs
|
||||
|
||||
**Anti-Patterns:**
|
||||
- ❌ Ignore findings → ✅ Investigate each
|
||||
- ❌ Full scan per edit → ✅ Scope to file
|
||||
- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`)
|
||||
````
|
||||
50
.opencode/rules
Normal file
50
.opencode/rules
Normal file
@@ -0,0 +1,50 @@
|
||||
|
||||
````markdown
|
||||
## UBS Quick Reference for AI Agents
|
||||
|
||||
UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On**
|
||||
|
||||
**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh | bash`
|
||||
|
||||
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
ubs file.ts file2.py # Specific files (< 1s) — USE THIS
|
||||
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||
ubs --only=js,python src/ # Language filter (3-5x faster)
|
||||
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||
ubs --help # Full command reference
|
||||
ubs sessions --entries 1 # Tail the latest install session log
|
||||
ubs . # Whole project (ignores things like .venv and node_modules automatically)
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```
|
||||
⚠️ Category (N errors)
|
||||
file.ts:42:5 – Issue description
|
||||
💡 Suggested fix
|
||||
Exit code: 1
|
||||
```
|
||||
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||
|
||||
**Fix Workflow:**
|
||||
1. Read finding → category + fix suggestion
|
||||
2. Navigate `file:line:col` → view context
|
||||
3. Verify real issue (not false positive)
|
||||
4. Fix root cause (not symptom)
|
||||
5. Re-run `ubs <file>` → exit 0
|
||||
6. Commit
|
||||
|
||||
**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits.
|
||||
|
||||
**Bug Severity:**
|
||||
- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks
|
||||
- **Important** (production): Type narrowing, division-by-zero, resource leaks
|
||||
- **Contextual** (judgment): TODO/FIXME, console logs
|
||||
|
||||
**Anti-Patterns:**
|
||||
- ❌ Ignore findings → ✅ Investigate each
|
||||
- ❌ Full scan per edit → ✅ Scope to file
|
||||
- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`)
|
||||
````
|
||||
151
AGENTS.md
151
AGENTS.md
@@ -14,6 +14,47 @@ If I tell you to do something, even if it goes against what follows below, YOU M
|
||||
|
||||
---
|
||||
|
||||
## Version Control: jj-First (CRITICAL)
|
||||
|
||||
**ALWAYS prefer jj (Jujutsu) over git for VCS mutations** (commit, describe, rebase, push, bookmark, undo). This is a colocated repo with both `.jj/` and `.git/`. Only fall back to raw `git` for things jj cannot do (hooks, LFS, submodules, `gh` CLI interop).
|
||||
|
||||
**Exception — read-only inspection:** Use `git status`, `git diff`, `git log` instead of their jj equivalents. In a colocated repo these see accurate data, and unlike jj, they don't create operations that cause divergences when multiple agents run concurrently. See "Parallel Agent VCS Protocol" below.
|
||||
|
||||
See `~/.claude/rules/jj-vcs/` for the full command reference, translation table, revsets, patterns, and recovery recipes.
|
||||
|
||||
### Parallel Agent VCS Protocol (CRITICAL)
|
||||
|
||||
Multiple agents often run concurrently in separate terminal panes, sharing the same repo directory. This requires care because jj's auto-snapshot creates operations on EVERY command — even read-only ones like `jj status`. Concurrent jj commands fork from the same parent operation and create **divergent changes**.
|
||||
|
||||
**The rule: use git for reads, jj for writes.**
|
||||
|
||||
In a colocated repo, git reads see accurate data because jj keeps `.git/` in sync.
|
||||
|
||||
| Operation | Use | Why |
|
||||
|-----------|-----|-----|
|
||||
| Check status | `git status` | No jj operation created |
|
||||
| View diff | `git diff` | No jj operation created |
|
||||
| Browse history | `git log` | No jj operation created |
|
||||
| Commit work | `jj commit -m "msg"` | jj mutation (better UX) |
|
||||
| Update description | `jj describe -m "msg"` | jj mutation |
|
||||
| Rebase | `jj rebase -d trunk()` | jj mutation |
|
||||
| Push | `jj git push -b <name>` | jj mutation |
|
||||
| Manage bookmarks | `jj bookmark set ...` | jj mutation |
|
||||
| Undo a mistake | `jj undo` | jj mutation |
|
||||
|
||||
**NEVER run `jj status`, `jj diff`, `jj log`, or `jj show` when other agents may be active** — these trigger snapshots that cause divergences.
|
||||
|
||||
**If using Claude Code's built-in agent teams:** Only the team lead runs ANY VCS commands (git or jj). Workers only edit files via Edit/Write tools and do NOT run "Landing the Plane".
|
||||
|
||||
**Resolving divergences if they occur:**
|
||||
|
||||
```bash
|
||||
jj log -r 'divergent()' # Find divergent changes
|
||||
jj abandon <unwanted-commit-id> # Keep the version you want
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Irreversible Git & Filesystem Actions — DO NOT EVER BREAK GLASS
|
||||
|
||||
> **Note:** Treat destructive commands as break-glass. If there's any doubt, stop and ask.
|
||||
@@ -316,7 +357,7 @@ bv --robot-insights | jq '.Cycles' # Circular deps (must
|
||||
|
||||
```bash
|
||||
ubs file.rs file2.rs # Specific files (< 1s) — USE THIS
|
||||
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||
ubs $(jj diff --name-only) # Changed files — before commit
|
||||
ubs --only=rust,toml src/ # Language filter (3-5x faster)
|
||||
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||
ubs . # Whole project (ignores target/, Cargo.lock)
|
||||
@@ -428,9 +469,9 @@ Returns structured results with file paths, line ranges, and extracted code snip
|
||||
|
||||
## Beads Workflow Integration
|
||||
|
||||
This project uses [beads_viewer](https://github.com/Dicklesworthstone/beads_viewer) for issue tracking. Issues are stored in `.beads/` and tracked in git.
|
||||
This project uses [beads_viewer](https://github.com/Dicklesworthstone/beads_viewer) for issue tracking. Issues are stored in `.beads/` and tracked in version control.
|
||||
|
||||
**Note:** `br` is non-invasive—it never executes git commands directly. You must run git commands manually after `br sync --flush-only`.
|
||||
**Note:** `br` is non-invasive—it never executes VCS commands directly. You must commit manually after `br sync --flush-only`.
|
||||
|
||||
### Essential Commands
|
||||
|
||||
@@ -446,7 +487,7 @@ br create --title="..." --type=task --priority=2
|
||||
br update <id> --status=in_progress
|
||||
br close <id> --reason="Completed"
|
||||
br close <id1> <id2> # Close multiple issues at once
|
||||
br sync --flush-only # Export to JSONL (then manually: git add .beads/ && git commit)
|
||||
br sync --flush-only # Export to JSONL (then: jj commit -m "Update beads")
|
||||
```
|
||||
|
||||
### Workflow Pattern
|
||||
@@ -466,15 +507,14 @@ br sync --flush-only # Export to JSONL (then manually: git add .beads/ && git c
|
||||
|
||||
### Session Protocol
|
||||
|
||||
**Before ending any session, run this checklist:**
|
||||
**Before ending any session, run this checklist (solo/lead only — workers skip VCS):**
|
||||
|
||||
```bash
|
||||
git status # Check what changed
|
||||
git add <files> # Stage code changes
|
||||
jj status # Check what changed
|
||||
br sync --flush-only # Export beads to JSONL
|
||||
git add .beads/ # Stage beads changes
|
||||
git commit -m "..." # Commit code and beads
|
||||
git push # Push to remote
|
||||
jj commit -m "..." # Commit code and beads (jj auto-tracks all changes)
|
||||
jj bookmark set <name> -r @- # Point bookmark at committed work
|
||||
jj git push -b <name> # Push to remote
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
@@ -483,13 +523,15 @@ git push # Push to remote
|
||||
- Update status as you work (in_progress → closed)
|
||||
- Create new issues with `br create` when you discover tasks
|
||||
- Use descriptive titles and set appropriate priority/type
|
||||
- Always run `br sync --flush-only` then commit .beads/ before ending session
|
||||
- Always run `br sync --flush-only` then commit before ending session (jj auto-tracks .beads/)
|
||||
|
||||
<!-- end-bv-agent-instructions -->
|
||||
|
||||
## Landing the Plane (Session Completion)
|
||||
|
||||
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
|
||||
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until push succeeds.
|
||||
|
||||
**WHO RUNS THIS:** Solo agents run it themselves. In multi-agent sessions, ONLY the team lead runs this. Workers skip VCS entirely.
|
||||
|
||||
**MANDATORY WORKFLOW:**
|
||||
|
||||
@@ -498,19 +540,20 @@ git push # Push to remote
|
||||
3. **Update issue status** - Close finished work, update in-progress items
|
||||
4. **PUSH TO REMOTE** - This is MANDATORY:
|
||||
```bash
|
||||
git pull --rebase
|
||||
br sync --flush-only
|
||||
git add .beads/
|
||||
git commit -m "Update beads"
|
||||
git push
|
||||
git status # MUST show "up to date with origin"
|
||||
jj git fetch # Get latest remote state
|
||||
jj rebase -d trunk() # Rebase onto latest trunk if needed
|
||||
br sync --flush-only # Export beads to JSONL
|
||||
jj commit -m "Update beads" # Commit (jj auto-tracks .beads/ changes)
|
||||
jj bookmark set <name> -r @- # Point bookmark at committed work
|
||||
jj git push -b <name> # Push to remote
|
||||
jj log -r '<name>' # Verify bookmark position
|
||||
```
|
||||
5. **Clean up** - Clear stashes, prune remote branches
|
||||
5. **Clean up** - Abandon empty orphan changes if any (`jj abandon <rev>`)
|
||||
6. **Verify** - All changes committed AND pushed
|
||||
7. **Hand off** - Provide context for next session
|
||||
|
||||
**CRITICAL RULES:**
|
||||
- Work is NOT complete until `git push` succeeds
|
||||
- Work is NOT complete until `jj git push` succeeds
|
||||
- NEVER stop before pushing - that leaves work stranded locally
|
||||
- NEVER say "ready to push when you are" - YOU must push
|
||||
- If push fails, resolve and retry until it succeeds
|
||||
@@ -618,6 +661,9 @@ LORE_ROBOT=1 lore issues
|
||||
lore --robot issues -n 10
|
||||
lore --robot mrs -s opened
|
||||
|
||||
# Filter issues by work item status (case-insensitive)
|
||||
lore --robot issues --status "In progress"
|
||||
|
||||
# List with field selection (reduces token usage ~60%)
|
||||
lore --robot issues --fields minimal
|
||||
lore --robot mrs --fields iid,title,state,draft
|
||||
@@ -740,3 +786,68 @@ lore -J mrs --fields iid,title,state,draft,labels # Custom field list
|
||||
- Use `lore --robot health` as a fast pre-flight check before queries
|
||||
- Use `lore robot-docs` for response schema discovery
|
||||
- The `-p` flag supports fuzzy project matching (suffix and substring)
|
||||
|
||||
---
|
||||
|
||||
## Read/Write Split: lore vs glab
|
||||
|
||||
| Operation | Tool | Why |
|
||||
|-----------|------|-----|
|
||||
| List issues/MRs | lore | Richer: includes status, discussions, closing MRs |
|
||||
| View issue/MR detail | lore | Pre-joined discussions, work-item status |
|
||||
| Search across entities | lore | FTS5 + vector hybrid search |
|
||||
| Expert/workload analysis | lore | who command — no glab equivalent |
|
||||
| Timeline reconstruction | lore | Chronological narrative — no glab equivalent |
|
||||
| Create/update/close | glab | Write operations |
|
||||
| Approve/merge MR | glab | Write operations |
|
||||
| CI/CD pipelines | glab | Not in lore scope |
|
||||
|
||||
````markdown
|
||||
## UBS Quick Reference for AI Agents
|
||||
|
||||
UBS stands for "Ultimate Bug Scanner": **The AI Coding Agent's Secret Weapon: Flagging Likely Bugs for Fixing Early On**
|
||||
|
||||
**Install:** `curl -sSL https://raw.githubusercontent.com/Dicklesworthstone/ultimate_bug_scanner/master/install.sh | bash`
|
||||
|
||||
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||
|
||||
**Commands:**
|
||||
```bash
|
||||
ubs file.ts file2.py # Specific files (< 1s) — USE THIS
|
||||
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||
ubs --only=js,python src/ # Language filter (3-5x faster)
|
||||
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||
ubs --help # Full command reference
|
||||
ubs sessions --entries 1 # Tail the latest install session log
|
||||
ubs . # Whole project (ignores things like .venv and node_modules automatically)
|
||||
```
|
||||
|
||||
**Output Format:**
|
||||
```
|
||||
⚠️ Category (N errors)
|
||||
file.ts:42:5 – Issue description
|
||||
💡 Suggested fix
|
||||
Exit code: 1
|
||||
```
|
||||
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||
|
||||
**Fix Workflow:**
|
||||
1. Read finding → category + fix suggestion
|
||||
2. Navigate `file:line:col` → view context
|
||||
3. Verify real issue (not false positive)
|
||||
4. Fix root cause (not symptom)
|
||||
5. Re-run `ubs <file>` → exit 0
|
||||
6. Commit
|
||||
|
||||
**Speed Critical:** Scope to changed files. `ubs src/file.ts` (< 1s) vs `ubs .` (30s). Never full scan for small edits.
|
||||
|
||||
**Bug Severity:**
|
||||
- **Critical** (always fix): Null safety, XSS/injection, async/await, memory leaks
|
||||
- **Important** (production): Type narrowing, division-by-zero, resource leaks
|
||||
- **Contextual** (judgment): TODO/FIXME, console logs
|
||||
|
||||
**Anti-Patterns:**
|
||||
- ❌ Ignore findings → ✅ Investigate each
|
||||
- ❌ Full scan per edit → ✅ Scope to file
|
||||
- ❌ Fix symptom (`if (x) { x.y }`) → ✅ Root cause (`x?.y`)
|
||||
````
|
||||
|
||||
742
AGENTS.md.backup
Normal file
742
AGENTS.md.backup
Normal file
@@ -0,0 +1,742 @@
|
||||
# AGENTS.md
|
||||
|
||||
## RULE 0 - THE FUNDAMENTAL OVERRIDE PEROGATIVE
|
||||
|
||||
If I tell you to do something, even if it goes against what follows below, YOU MUST LISTEN TO ME. I AM IN CHARGE, NOT YOU.
|
||||
|
||||
---
|
||||
|
||||
## RULE NUMBER 1: NO FILE DELETION
|
||||
|
||||
**YOU ARE NEVER ALLOWED TO DELETE A FILE WITHOUT EXPRESS PERMISSION.** Even a new file that you yourself created, such as a test code file. You have a horrible track record of deleting critically important files or otherwise throwing away tons of expensive work. As a result, you have permanently lost any and all rights to determine that a file or folder should be deleted.
|
||||
|
||||
**YOU MUST ALWAYS ASK AND RECEIVE CLEAR, WRITTEN PERMISSION BEFORE EVER DELETING A FILE OR FOLDER OF ANY KIND.**
|
||||
|
||||
---
|
||||
|
||||
## Irreversible Git & Filesystem Actions — DO NOT EVER BREAK GLASS
|
||||
|
||||
> **Note:** Treat destructive commands as break-glass. If there's any doubt, stop and ask.
|
||||
|
||||
1. **Absolutely forbidden commands:** `git reset --hard`, `git clean -fd`, `rm -rf`, or any command that can delete or overwrite code/data must never be run unless the user explicitly provides the exact command and states, in the same message, that they understand and want the irreversible consequences.
|
||||
2. **No guessing:** If there is any uncertainty about what a command might delete or overwrite, stop immediately and ask the user for specific approval. "I think it's safe" is never acceptable.
|
||||
3. **Safer alternatives first:** When cleanup or rollbacks are needed, request permission to use non-destructive options (`git status`, `git diff`, `git stash`, copying to backups) before ever considering a destructive command.
|
||||
4. **Mandatory explicit plan:** Even after explicit user authorization, restate the command verbatim, list exactly what will be affected, and wait for a confirmation that your understanding is correct. Only then may you execute it—if anything remains ambiguous, refuse and escalate.
|
||||
5. **Document the confirmation:** When running any approved destructive command, record (in the session notes / final response) the exact user text that authorized it, the command actually run, and the execution time. If that record is absent, the operation did not happen.
|
||||
|
||||
---
|
||||
|
||||
## Toolchain: Rust & Cargo
|
||||
|
||||
We only use **Cargo** in this project, NEVER any other package manager.
|
||||
|
||||
- **Edition/toolchain:** Follow `rust-toolchain.toml` (if present). Do not assume stable vs nightly.
|
||||
- **Dependencies:** Explicit versions for stability; keep the set minimal.
|
||||
- **Configuration:** Cargo.toml only
|
||||
- **Unsafe code:** Forbidden (`#![forbid(unsafe_code)]`)
|
||||
|
||||
When writing Rust code, reference RUST_CLI_TOOLS_BEST_PRACTICES.md
|
||||
|
||||
### Release Profile
|
||||
|
||||
Use the release profile defined in `Cargo.toml`. If you need to change it, justify the
|
||||
performance/size tradeoff and how it impacts determinism and cancellation behavior.
|
||||
|
||||
---
|
||||
|
||||
## Code Editing Discipline
|
||||
|
||||
### No Script-Based Changes
|
||||
|
||||
**NEVER** run a script that processes/changes code files in this repo. Brittle regex-based transformations create far more problems than they solve.
|
||||
|
||||
- **Always make code changes manually**, even when there are many instances
|
||||
- For many simple changes: use parallel subagents
|
||||
- For subtle/complex changes: do them methodically yourself
|
||||
|
||||
### No File Proliferation
|
||||
|
||||
If you want to change something or add a feature, **revise existing code files in place**.
|
||||
|
||||
**NEVER** create variations like:
|
||||
- `mainV2.rs`
|
||||
- `main_improved.rs`
|
||||
- `main_enhanced.rs`
|
||||
|
||||
New files are reserved for **genuinely new functionality** that makes zero sense to include in any existing file. The bar for creating new files is **incredibly high**.
|
||||
|
||||
---
|
||||
|
||||
## Backwards Compatibility
|
||||
|
||||
We do not care about backwards compatibility—we're in early development with no users. We want to do things the **RIGHT** way with **NO TECH DEBT**.
|
||||
|
||||
- Never create "compatibility shims"
|
||||
- Never create wrapper functions for deprecated APIs
|
||||
- Just fix the code directly
|
||||
|
||||
---
|
||||
|
||||
## Compiler Checks (CRITICAL)
|
||||
|
||||
**After any substantive code changes, you MUST verify no errors were introduced:**
|
||||
|
||||
```bash
|
||||
# Check for compiler errors and warnings
|
||||
cargo check --all-targets
|
||||
|
||||
# Check for clippy lints (pedantic + nursery are enabled)
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
|
||||
# Verify formatting
|
||||
cargo fmt --check
|
||||
```
|
||||
|
||||
If you see errors, **carefully understand and resolve each issue**. Read sufficient context to fix them the RIGHT way.
|
||||
|
||||
---
|
||||
|
||||
## Testing
|
||||
|
||||
### Unit & Property Tests
|
||||
|
||||
```bash
|
||||
# Run all tests
|
||||
cargo test
|
||||
|
||||
# Run with output
|
||||
cargo test -- --nocapture
|
||||
```
|
||||
|
||||
When adding or changing primitives, add tests that assert the core invariants:
|
||||
|
||||
- no task leaks
|
||||
- no obligation leaks
|
||||
- losers are drained after races
|
||||
- region close implies quiescence
|
||||
|
||||
Prefer deterministic lab-runtime tests for concurrency-sensitive behavior.
|
||||
|
||||
---
|
||||
|
||||
## MCP Agent Mail — Multi-Agent Coordination
|
||||
|
||||
A mail-like layer that lets coding agents coordinate asynchronously via MCP tools and resources. Provides identities, inbox/outbox, searchable threads, and advisory file reservations with human-auditable artifacts in Git.
|
||||
|
||||
### Why It's Useful
|
||||
|
||||
- **Prevents conflicts:** Explicit file reservations (leases) for files/globs
|
||||
- **Token-efficient:** Messages stored in per-project archive, not in context
|
||||
- **Quick reads:** `resource://inbox/...`, `resource://thread/...`
|
||||
|
||||
### Same Repository Workflow
|
||||
|
||||
1. **Register identity:**
|
||||
```
|
||||
ensure_project(project_key=<abs-path>)
|
||||
register_agent(project_key, program, model)
|
||||
```
|
||||
|
||||
2. **Reserve files before editing:**
|
||||
```
|
||||
file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true)
|
||||
```
|
||||
|
||||
3. **Communicate with threads:**
|
||||
```
|
||||
send_message(..., thread_id="FEAT-123")
|
||||
fetch_inbox(project_key, agent_name)
|
||||
acknowledge_message(project_key, agent_name, message_id)
|
||||
```
|
||||
|
||||
4. **Quick reads:**
|
||||
```
|
||||
resource://inbox/{Agent}?project=<abs-path>&limit=20
|
||||
resource://thread/{id}?project=<abs-path>&include_bodies=true
|
||||
```
|
||||
|
||||
### Macros vs Granular Tools
|
||||
|
||||
- **Prefer macros for speed:** `macro_start_session`, `macro_prepare_thread`, `macro_file_reservation_cycle`, `macro_contact_handshake`
|
||||
- **Use granular tools for control:** `register_agent`, `file_reservation_paths`, `send_message`, `fetch_inbox`, `acknowledge_message`
|
||||
|
||||
### Common Pitfalls
|
||||
|
||||
- `"from_agent not registered"`: Always `register_agent` in the correct `project_key` first
|
||||
- `"FILE_RESERVATION_CONFLICT"`: Adjust patterns, wait for expiry, or use non-exclusive reservation
|
||||
- **Auth errors:** If JWT+JWKS enabled, include bearer token with matching `kid`
|
||||
|
||||
---
|
||||
|
||||
## Beads (br) — Dependency-Aware Issue Tracking
|
||||
|
||||
Beads provides a lightweight, dependency-aware issue database and CLI (`br` / beads_rust) for selecting "ready work," setting priorities, and tracking status. It complements MCP Agent Mail's messaging and file reservations.
|
||||
|
||||
**Note:** `br` is non-invasive—it never executes git commands directly. You must run git commands manually after `br sync --flush-only`.
|
||||
|
||||
### Conventions
|
||||
|
||||
- **Single source of truth:** Beads for task status/priority/dependencies; Agent Mail for conversation and audit
|
||||
- **Shared identifiers:** Use Beads issue ID (e.g., `br-123`) as Mail `thread_id` and prefix subjects with `[br-123]`
|
||||
- **Reservations:** When starting a task, call `file_reservation_paths()` with the issue ID in `reason`
|
||||
|
||||
### Typical Agent Flow
|
||||
|
||||
1. **Pick ready work (Beads):**
|
||||
```bash
|
||||
br ready --json # Choose highest priority, no blockers
|
||||
```
|
||||
|
||||
2. **Reserve edit surface (Mail):**
|
||||
```
|
||||
file_reservation_paths(project_key, agent_name, ["src/**"], ttl_seconds=3600, exclusive=true, reason="br-123")
|
||||
```
|
||||
|
||||
3. **Announce start (Mail):**
|
||||
```
|
||||
send_message(..., thread_id="br-123", subject="[br-123] Start: <title>", ack_required=true)
|
||||
```
|
||||
|
||||
4. **Work and update:** Reply in-thread with progress
|
||||
|
||||
5. **Complete and release:**
|
||||
```bash
|
||||
br close br-123 --reason "Completed"
|
||||
```
|
||||
```
|
||||
release_file_reservations(project_key, agent_name, paths=["src/**"])
|
||||
```
|
||||
Final Mail reply: `[br-123] Completed` with summary
|
||||
|
||||
### Mapping Cheat Sheet
|
||||
|
||||
| Concept | Value |
|
||||
|---------|-------|
|
||||
| Mail `thread_id` | `br-###` |
|
||||
| Mail subject | `[br-###] ...` |
|
||||
| File reservation `reason` | `br-###` |
|
||||
| Commit messages | Include `br-###` for traceability |
|
||||
|
||||
---
|
||||
|
||||
## bv — Graph-Aware Triage Engine
|
||||
|
||||
bv is a graph-aware triage engine for Beads projects (`.beads/beads.jsonl`). It computes PageRank, betweenness, critical path, cycles, HITS, eigenvector, and k-core metrics deterministically.
|
||||
|
||||
**Scope boundary:** bv handles *what to work on* (triage, priority, planning). For agent-to-agent coordination (messaging, work claiming, file reservations), use MCP Agent Mail.
|
||||
|
||||
**CRITICAL: Use ONLY `--robot-*` flags. Bare `bv` launches an interactive TUI that blocks your session.**
|
||||
|
||||
### The Workflow: Start With Triage
|
||||
|
||||
**`bv --robot-triage` is your single entry point.** It returns:
|
||||
- `quick_ref`: at-a-glance counts + top 3 picks
|
||||
- `recommendations`: ranked actionable items with scores, reasons, unblock info
|
||||
- `quick_wins`: low-effort high-impact items
|
||||
- `blockers_to_clear`: items that unblock the most downstream work
|
||||
- `project_health`: status/type/priority distributions, graph metrics
|
||||
- `commands`: copy-paste shell commands for next steps
|
||||
|
||||
```bash
|
||||
bv --robot-triage # THE MEGA-COMMAND: start here
|
||||
bv --robot-next # Minimal: just the single top pick + claim command
|
||||
```
|
||||
|
||||
### Command Reference
|
||||
|
||||
**Planning:**
|
||||
| Command | Returns |
|
||||
|---------|---------|
|
||||
| `--robot-plan` | Parallel execution tracks with `unblocks` lists |
|
||||
| `--robot-priority` | Priority misalignment detection with confidence |
|
||||
|
||||
**Graph Analysis:**
|
||||
| Command | Returns |
|
||||
|---------|---------|
|
||||
| `--robot-insights` | Full metrics: PageRank, betweenness, HITS, eigenvector, critical path, cycles, k-core, articulation points, slack |
|
||||
| `--robot-label-health` | Per-label health: `health_level`, `velocity_score`, `staleness`, `blocked_count` |
|
||||
| `--robot-label-flow` | Cross-label dependency: `flow_matrix`, `dependencies`, `bottleneck_labels` |
|
||||
| `--robot-label-attention [--attention-limit=N]` | Attention-ranked labels |
|
||||
|
||||
**History & Change Tracking:**
|
||||
| Command | Returns |
|
||||
|---------|---------|
|
||||
| `--robot-history` | Bead-to-commit correlations |
|
||||
| `--robot-diff --diff-since <ref>` | Changes since ref: new/closed/modified issues, cycles |
|
||||
|
||||
**Other:**
|
||||
| Command | Returns |
|
||||
|---------|---------|
|
||||
| `--robot-burndown <sprint>` | Sprint burndown, scope changes, at-risk items |
|
||||
| `--robot-forecast <id\|all>` | ETA predictions with dependency-aware scheduling |
|
||||
| `--robot-alerts` | Stale issues, blocking cascades, priority mismatches |
|
||||
| `--robot-suggest` | Hygiene: duplicates, missing deps, label suggestions |
|
||||
| `--robot-graph [--graph-format=json\|dot\|mermaid]` | Dependency graph export |
|
||||
| `--export-graph <file.html>` | Interactive HTML visualization |
|
||||
|
||||
### Scoping & Filtering
|
||||
|
||||
```bash
|
||||
bv --robot-plan --label backend # Scope to label's subgraph
|
||||
bv --robot-insights --as-of HEAD~30 # Historical point-in-time
|
||||
bv --recipe actionable --robot-plan # Pre-filter: ready to work
|
||||
bv --recipe high-impact --robot-triage # Pre-filter: top PageRank
|
||||
bv --robot-triage --robot-triage-by-track # Group by parallel work streams
|
||||
bv --robot-triage --robot-triage-by-label # Group by domain
|
||||
```
|
||||
|
||||
### Understanding Robot Output
|
||||
|
||||
**All robot JSON includes:**
|
||||
- `data_hash` — Fingerprint of source beads.jsonl
|
||||
- `status` — Per-metric state: `computed|approx|timeout|skipped` + elapsed ms
|
||||
- `as_of` / `as_of_commit` — Present when using `--as-of`
|
||||
|
||||
**Two-phase analysis:**
|
||||
- **Phase 1 (instant):** degree, topo sort, density
|
||||
- **Phase 2 (async, 500ms timeout):** PageRank, betweenness, HITS, eigenvector, cycles
|
||||
|
||||
### jq Quick Reference
|
||||
|
||||
```bash
|
||||
bv --robot-triage | jq '.quick_ref' # At-a-glance summary
|
||||
bv --robot-triage | jq '.recommendations[0]' # Top recommendation
|
||||
bv --robot-plan | jq '.plan.summary.highest_impact' # Best unblock target
|
||||
bv --robot-insights | jq '.status' # Check metric readiness
|
||||
bv --robot-insights | jq '.Cycles' # Circular deps (must fix!)
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## UBS — Ultimate Bug Scanner
|
||||
|
||||
**Golden Rule:** `ubs <changed-files>` before every commit. Exit 0 = safe. Exit >0 = fix & re-run.
|
||||
|
||||
### Commands
|
||||
|
||||
```bash
|
||||
ubs file.rs file2.rs # Specific files (< 1s) — USE THIS
|
||||
ubs $(git diff --name-only --cached) # Staged files — before commit
|
||||
ubs --only=rust,toml src/ # Language filter (3-5x faster)
|
||||
ubs --ci --fail-on-warning . # CI mode — before PR
|
||||
ubs . # Whole project (ignores target/, Cargo.lock)
|
||||
```
|
||||
|
||||
### Output Format
|
||||
|
||||
```
|
||||
⚠️ Category (N errors)
|
||||
file.rs:42:5 – Issue description
|
||||
💡 Suggested fix
|
||||
Exit code: 1
|
||||
```
|
||||
|
||||
Parse: `file:line:col` → location | 💡 → how to fix | Exit 0/1 → pass/fail
|
||||
|
||||
### Fix Workflow
|
||||
|
||||
1. Read finding → category + fix suggestion
|
||||
2. Navigate `file:line:col` → view context
|
||||
3. Verify real issue (not false positive)
|
||||
4. Fix root cause (not symptom)
|
||||
5. Re-run `ubs <file>` → exit 0
|
||||
6. Commit
|
||||
|
||||
### Bug Severity
|
||||
|
||||
- **Critical (always fix):** Memory safety, use-after-free, data races, SQL injection
|
||||
- **Important (production):** Unwrap panics, resource leaks, overflow checks
|
||||
- **Contextual (judgment):** TODO/FIXME, println! debugging
|
||||
|
||||
---
|
||||
|
||||
## ast-grep vs ripgrep
|
||||
|
||||
**Use `ast-grep` when structure matters.** It parses code and matches AST nodes, ignoring comments/strings, and can **safely rewrite** code.
|
||||
|
||||
- Refactors/codemods: rename APIs, change import forms
|
||||
- Policy checks: enforce patterns across a repo
|
||||
- Editor/automation: LSP mode, `--json` output
|
||||
|
||||
**Use `ripgrep` when text is enough.** Fastest way to grep literals/regex.
|
||||
|
||||
- Recon: find strings, TODOs, log lines, config values
|
||||
- Pre-filter: narrow candidate files before ast-grep
|
||||
|
||||
### Rule of Thumb
|
||||
|
||||
- Need correctness or **applying changes** → `ast-grep`
|
||||
- Need raw speed or **hunting text** → `rg`
|
||||
- Often combine: `rg` to shortlist files, then `ast-grep` to match/modify
|
||||
|
||||
### Rust Examples
|
||||
|
||||
```bash
|
||||
# Find structured code (ignores comments)
|
||||
ast-grep run -l Rust -p 'fn $NAME($$$ARGS) -> $RET { $$$BODY }'
|
||||
|
||||
# Find all unwrap() calls
|
||||
ast-grep run -l Rust -p '$EXPR.unwrap()'
|
||||
|
||||
# Quick textual hunt
|
||||
rg -n 'println!' -t rust
|
||||
|
||||
# Combine speed + precision
|
||||
rg -l -t rust 'unwrap\(' | xargs ast-grep run -l Rust -p '$X.unwrap()' --json
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Morph Warp Grep — AI-Powered Code Search
|
||||
|
||||
**Use `mcp__morph-mcp__warp_grep` for exploratory "how does X work?" questions.** An AI agent expands your query, greps the codebase, reads relevant files, and returns precise line ranges with full context.
|
||||
|
||||
**Use `ripgrep` for targeted searches.** When you know exactly what you're looking for.
|
||||
|
||||
**Use `ast-grep` for structural patterns.** When you need AST precision for matching/rewriting.
|
||||
|
||||
### When to Use What
|
||||
|
||||
| Scenario | Tool | Why |
|
||||
|----------|------|-----|
|
||||
| "How is pattern matching implemented?" | `warp_grep` | Exploratory; don't know where to start |
|
||||
| "Where is the quick reject filter?" | `warp_grep` | Need to understand architecture |
|
||||
| "Find all uses of `Regex::new`" | `ripgrep` | Targeted literal search |
|
||||
| "Find files with `println!`" | `ripgrep` | Simple pattern |
|
||||
| "Replace all `unwrap()` with `expect()`" | `ast-grep` | Structural refactor |
|
||||
|
||||
### warp_grep Usage
|
||||
|
||||
```
|
||||
mcp__morph-mcp__warp_grep(
|
||||
repoPath: "/path/to/dcg",
|
||||
query: "How does the safe pattern whitelist work?"
|
||||
)
|
||||
```
|
||||
|
||||
Returns structured results with file paths, line ranges, and extracted code snippets.
|
||||
|
||||
### Anti-Patterns
|
||||
|
||||
- **Don't** use `warp_grep` to find a specific function name → use `ripgrep`
|
||||
- **Don't** use `ripgrep` to understand "how does X work" → wastes time with manual reads
|
||||
- **Don't** use `ripgrep` for codemods → risks collateral edits
|
||||
|
||||
<!-- bv-agent-instructions-v1 -->
|
||||
|
||||
---
|
||||
|
||||
## Beads Workflow Integration
|
||||
|
||||
This project uses [beads_viewer](https://github.com/Dicklesworthstone/beads_viewer) for issue tracking. Issues are stored in `.beads/` and tracked in git.
|
||||
|
||||
**Note:** `br` is non-invasive—it never executes git commands directly. You must run git commands manually after `br sync --flush-only`.
|
||||
|
||||
### Essential Commands
|
||||
|
||||
```bash
|
||||
# View issues (launches TUI - avoid in automated sessions)
|
||||
bv
|
||||
|
||||
# CLI commands for agents (use these instead)
|
||||
br ready # Show issues ready to work (no blockers)
|
||||
br list --status=open # All open issues
|
||||
br show <id> # Full issue details with dependencies
|
||||
br create --title="..." --type=task --priority=2
|
||||
br update <id> --status=in_progress
|
||||
br close <id> --reason="Completed"
|
||||
br close <id1> <id2> # Close multiple issues at once
|
||||
br sync --flush-only # Export to JSONL (then manually: git add .beads/ && git commit)
|
||||
```
|
||||
|
||||
### Workflow Pattern
|
||||
|
||||
1. **Start**: Run `br ready` to find actionable work
|
||||
2. **Claim**: Use `br update <id> --status=in_progress`
|
||||
3. **Work**: Implement the task
|
||||
4. **Complete**: Use `br close <id>`
|
||||
5. **Sync**: Run `br sync --flush-only`, then `git add .beads/ && git commit -m "Update beads"`
|
||||
|
||||
### Key Concepts
|
||||
|
||||
- **Dependencies**: Issues can block other issues. `br ready` shows only unblocked work.
|
||||
- **Priority**: P0=critical, P1=high, P2=medium, P3=low, P4=backlog (use numbers, not words)
|
||||
- **Types**: task, bug, feature, epic, question, docs
|
||||
- **Blocking**: `br dep add <issue> <depends-on>` to add dependencies
|
||||
|
||||
### Session Protocol
|
||||
|
||||
**Before ending any session, run this checklist:**
|
||||
|
||||
```bash
|
||||
git status # Check what changed
|
||||
git add <files> # Stage code changes
|
||||
br sync --flush-only # Export beads to JSONL
|
||||
git add .beads/ # Stage beads changes
|
||||
git commit -m "..." # Commit code and beads
|
||||
git push # Push to remote
|
||||
```
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Check `br ready` at session start to find available work
|
||||
- Update status as you work (in_progress → closed)
|
||||
- Create new issues with `br create` when you discover tasks
|
||||
- Use descriptive titles and set appropriate priority/type
|
||||
- Always run `br sync --flush-only` then commit .beads/ before ending session
|
||||
|
||||
<!-- end-bv-agent-instructions -->
|
||||
|
||||
## Landing the Plane (Session Completion)
|
||||
|
||||
**When ending a work session**, you MUST complete ALL steps below. Work is NOT complete until `git push` succeeds.
|
||||
|
||||
**MANDATORY WORKFLOW:**
|
||||
|
||||
1. **File issues for remaining work** - Create issues for anything that needs follow-up
|
||||
2. **Run quality gates** (if code changed) - Tests, linters, builds
|
||||
3. **Update issue status** - Close finished work, update in-progress items
|
||||
4. **PUSH TO REMOTE** - This is MANDATORY:
|
||||
```bash
|
||||
git pull --rebase
|
||||
br sync --flush-only
|
||||
git add .beads/
|
||||
git commit -m "Update beads"
|
||||
git push
|
||||
git status # MUST show "up to date with origin"
|
||||
```
|
||||
5. **Clean up** - Clear stashes, prune remote branches
|
||||
6. **Verify** - All changes committed AND pushed
|
||||
7. **Hand off** - Provide context for next session
|
||||
|
||||
**CRITICAL RULES:**
|
||||
- Work is NOT complete until `git push` succeeds
|
||||
- NEVER stop before pushing - that leaves work stranded locally
|
||||
- NEVER say "ready to push when you are" - YOU must push
|
||||
- If push fails, resolve and retry until it succeeds
|
||||
|
||||
---
|
||||
|
||||
## cass — Cross-Agent Session Search
|
||||
|
||||
`cass` indexes prior agent conversations (Claude Code, Codex, Cursor, Gemini, ChatGPT, etc.) so we can reuse solved problems.
|
||||
|
||||
**Rules:** Never run bare `cass` (TUI). Always use `--robot` or `--json`.
|
||||
|
||||
### Examples
|
||||
|
||||
```bash
|
||||
cass health
|
||||
cass search "async runtime" --robot --limit 5
|
||||
cass view /path/to/session.jsonl -n 42 --json
|
||||
cass expand /path/to/session.jsonl -n 42 -C 3 --json
|
||||
cass capabilities --json
|
||||
cass robot-docs guide
|
||||
```
|
||||
|
||||
### Tips
|
||||
|
||||
- Use `--fields minimal` for lean output
|
||||
- Filter by agent with `--agent`
|
||||
- Use `--days N` to limit to recent history
|
||||
|
||||
stdout is data-only, stderr is diagnostics; exit code 0 means success.
|
||||
|
||||
Treat cass as a way to avoid re-solving problems other agents already handled.
|
||||
|
||||
---
|
||||
|
||||
## Note for Codex/GPT-5.2
|
||||
|
||||
You constantly bother me and stop working with concerned questions that look similar to this:
|
||||
|
||||
```
|
||||
Unexpected changes (need guidance)
|
||||
|
||||
- Working tree still shows edits I did not make in Cargo.toml, Cargo.lock, src/runtime.rs, src/scope.rs. Please advise whether to keep/commit/revert these before any further work. I did not touch them.
|
||||
|
||||
Next steps (pick one)
|
||||
|
||||
1. Decide how to handle the unrelated modified files above so we can resume cleanly.
|
||||
```
|
||||
|
||||
NEVER EVER DO THAT AGAIN. The answer is literally ALWAYS the same: those are changes created by the potentially dozen of other agents working on the project at the same time. This is not only a common occurence, it happens multiple times PER MINUTE. The way to deal with it is simple: you NEVER, under ANY CIRCUMSTANCE, stash, revert, overwrite, or otherwise disturb in ANY way the work of other agents. Just treat those changes identically to changes that you yourself made. Just fool yourself into thinking YOU made the changes and simply don't recall it for some reason.
|
||||
|
||||
---
|
||||
|
||||
## Note on Built-in TODO Functionality
|
||||
|
||||
Also, if I ask you to explicitly use your built-in TODO functionality, don't complain about this and say you need to use beads. You can use built-in TODOs if I tell you specifically to do so. Always comply with such orders.
|
||||
|
||||
## TDD Requirements
|
||||
|
||||
Test-first development is mandatory:
|
||||
1. **RED** - Write failing test first
|
||||
2. **GREEN** - Minimal implementation to pass
|
||||
3. **REFACTOR** - Clean up while green
|
||||
|
||||
## Key Patterns
|
||||
|
||||
Find the simplest solution that meets all acceptance criteria.
|
||||
Use third party libraries whenever there's a well-maintained, active, and widely adopted solution (for example, date-fns for TS date math)
|
||||
Build extensible pieces of logic that can easily be integrated with other pieces.
|
||||
DRY principles should be loosely held.
|
||||
Architecture MUST be clear and well thought-out. Ask the user for clarification whenever ambiguity is discovered around architecture, or you think a better approach than planned exists.
|
||||
|
||||
---
|
||||
|
||||
## Third-Party Library Usage
|
||||
|
||||
If you aren't 100% sure how to use a third-party library, **SEARCH ONLINE** to find the latest documentation and mid-2025 best practices.
|
||||
|
||||
---
|
||||
|
||||
## Gitlore Robot Mode
|
||||
|
||||
The `lore` CLI has a robot mode optimized for AI agent consumption with compact JSON output, structured errors with machine-actionable recovery steps, meaningful exit codes, response timing metadata, field selection for token efficiency, and TTY auto-detection.
|
||||
|
||||
### Activation
|
||||
|
||||
```bash
|
||||
# Explicit flag
|
||||
lore --robot issues -n 10
|
||||
|
||||
# JSON shorthand (-J)
|
||||
lore -J issues -n 10
|
||||
|
||||
# Auto-detection (when stdout is not a TTY)
|
||||
lore issues | jq .
|
||||
|
||||
# Environment variable
|
||||
LORE_ROBOT=1 lore issues
|
||||
```
|
||||
|
||||
### Robot Mode Commands
|
||||
|
||||
```bash
|
||||
# List issues/MRs with JSON output
|
||||
lore --robot issues -n 10
|
||||
lore --robot mrs -s opened
|
||||
|
||||
# List with field selection (reduces token usage ~60%)
|
||||
lore --robot issues --fields minimal
|
||||
lore --robot mrs --fields iid,title,state,draft
|
||||
|
||||
# Show detailed entity info
|
||||
lore --robot issues 123
|
||||
lore --robot mrs 456 -p group/repo
|
||||
|
||||
# Count entities
|
||||
lore --robot count issues
|
||||
lore --robot count discussions --for mr
|
||||
|
||||
# Search indexed documents
|
||||
lore --robot search "authentication bug"
|
||||
|
||||
# Check sync status
|
||||
lore --robot status
|
||||
|
||||
# Run full sync pipeline
|
||||
lore --robot sync
|
||||
|
||||
# Run sync without resource events
|
||||
lore --robot sync --no-events
|
||||
|
||||
# Run ingestion only
|
||||
lore --robot ingest issues
|
||||
|
||||
# Check environment health
|
||||
lore --robot doctor
|
||||
|
||||
# Document and index statistics
|
||||
lore --robot stats
|
||||
|
||||
# Quick health pre-flight check (exit 0 = healthy, 19 = unhealthy)
|
||||
lore --robot health
|
||||
|
||||
# Generate searchable documents from ingested data
|
||||
lore --robot generate-docs
|
||||
|
||||
# Generate vector embeddings via Ollama
|
||||
lore --robot embed
|
||||
|
||||
# Agent self-discovery manifest (all commands, flags, exit codes, response schemas)
|
||||
lore robot-docs
|
||||
|
||||
# Version information
|
||||
lore --robot version
|
||||
```
|
||||
|
||||
### Response Format
|
||||
|
||||
All commands return compact JSON with a uniform envelope and timing metadata:
|
||||
|
||||
```json
|
||||
{"ok":true,"data":{...},"meta":{"elapsed_ms":42}}
|
||||
```
|
||||
|
||||
Errors return structured JSON to stderr with machine-actionable recovery steps:
|
||||
|
||||
```json
|
||||
{"error":{"code":"CONFIG_NOT_FOUND","message":"...","suggestion":"Run 'lore init'","actions":["lore init"]}}
|
||||
```
|
||||
|
||||
The `actions` array contains executable shell commands for automated recovery. It is omitted when empty.
|
||||
|
||||
### Field Selection
|
||||
|
||||
The `--fields` flag on `issues` and `mrs` list commands controls which fields appear in the JSON response:
|
||||
|
||||
```bash
|
||||
lore -J issues --fields minimal # Preset: iid, title, state, updated_at_iso
|
||||
lore -J mrs --fields iid,title,state,draft,labels # Custom field list
|
||||
```
|
||||
|
||||
### Exit Codes
|
||||
|
||||
| Code | Meaning |
|
||||
|------|---------|
|
||||
| 0 | Success |
|
||||
| 1 | Internal error / not implemented |
|
||||
| 2 | Usage error (invalid flags or arguments) |
|
||||
| 3 | Config invalid |
|
||||
| 4 | Token not set |
|
||||
| 5 | GitLab auth failed |
|
||||
| 6 | Resource not found |
|
||||
| 7 | Rate limited |
|
||||
| 8 | Network error |
|
||||
| 9 | Database locked |
|
||||
| 10 | Database error |
|
||||
| 11 | Migration failed |
|
||||
| 12 | I/O error |
|
||||
| 13 | Transform error |
|
||||
| 14 | Ollama unavailable |
|
||||
| 15 | Ollama model not found |
|
||||
| 16 | Embedding failed |
|
||||
| 17 | Not found (entity does not exist) |
|
||||
| 18 | Ambiguous match (use `-p` to specify project) |
|
||||
| 19 | Health check failed |
|
||||
| 20 | Config not found |
|
||||
|
||||
### Configuration Precedence
|
||||
|
||||
1. CLI flags (highest priority)
|
||||
2. Environment variables (`LORE_ROBOT`, `GITLAB_TOKEN`, `LORE_CONFIG_PATH`)
|
||||
3. Config file (`~/.config/lore/config.json`)
|
||||
4. Built-in defaults (lowest priority)
|
||||
|
||||
### Best Practices
|
||||
|
||||
- Use `lore --robot` or `lore -J` for all agent interactions
|
||||
- Check exit codes for error handling
|
||||
- Parse JSON errors from stderr; use `actions` array for automated recovery
|
||||
- Use `--fields minimal` to reduce token usage (~60% fewer tokens)
|
||||
- Use `-n` / `--limit` to control response size
|
||||
- Use `-q` / `--quiet` to suppress progress bars and non-essential output
|
||||
- Use `--color never` in non-TTY automation for ANSI-free output
|
||||
- Use `-v` / `-vv` / `-vvv` for increasing verbosity (debug/trace logging)
|
||||
- Use `--log-format json` for machine-readable log output to stderr
|
||||
- TTY detection handles piped commands automatically
|
||||
- Use `lore --robot health` as a fast pre-flight check before queries
|
||||
- Use `lore robot-docs` for response schema discovery
|
||||
- The `-p` flag supports fuzzy project matching (suffix and substring)
|
||||
3
Cargo.lock
generated
3
Cargo.lock
generated
@@ -1106,7 +1106,7 @@ checksum = "5e5032e24019045c762d3c0f28f5b6b8bbf38563a65908389bf7978758920897"
|
||||
|
||||
[[package]]
|
||||
name = "lore"
|
||||
version = "0.5.0"
|
||||
version = "0.6.2"
|
||||
dependencies = [
|
||||
"async-stream",
|
||||
"chrono",
|
||||
@@ -1118,6 +1118,7 @@ dependencies = [
|
||||
"dirs",
|
||||
"flate2",
|
||||
"futures",
|
||||
"httpdate",
|
||||
"indicatif",
|
||||
"libc",
|
||||
"open",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "lore"
|
||||
version = "0.5.0"
|
||||
version = "0.6.2"
|
||||
edition = "2024"
|
||||
description = "Gitlore - Local GitLab data management with semantic search"
|
||||
authors = ["Taylor Eernisse"]
|
||||
@@ -45,6 +45,7 @@ rand = "0.8"
|
||||
sha2 = "0.10"
|
||||
flate2 = "1"
|
||||
chrono = { version = "0.4", features = ["serde"] }
|
||||
httpdate = "1"
|
||||
uuid = { version = "1", features = ["v4"] }
|
||||
regex = "1"
|
||||
strsim = "0.11"
|
||||
|
||||
36
README.md
36
README.md
@@ -1,6 +1,6 @@
|
||||
# Gitlore
|
||||
|
||||
Local GitLab data management with semantic search, people intelligence, and temporal analysis. Syncs issues, MRs, discussions, and notes from GitLab to a local SQLite database for fast, offline-capable querying, filtering, hybrid search, chronological event reconstruction, and expert discovery.
|
||||
Local GitLab data management with semantic search, people intelligence, and temporal analysis. Syncs issues, MRs, discussions, notes, and work item statuses from GitLab to a local SQLite database for fast, offline-capable querying, filtering, hybrid search, chronological event reconstruction, and expert discovery.
|
||||
|
||||
## Features
|
||||
|
||||
@@ -8,7 +8,7 @@ Local GitLab data management with semantic search, people intelligence, and temp
|
||||
- **Incremental sync**: Cursor-based sync only fetches changes since last sync
|
||||
- **Full re-sync**: Reset cursors and fetch all data from scratch when needed
|
||||
- **Multi-project**: Track issues and MRs across multiple GitLab projects
|
||||
- **Rich filtering**: Filter by state, author, assignee, labels, milestone, due date, draft status, reviewer, branches
|
||||
- **Rich filtering**: Filter by state, author, assignee, labels, milestone, due date, draft status, reviewer, branches, work item status
|
||||
- **Hybrid search**: Combines FTS5 lexical search with Ollama-powered vector embeddings via Reciprocal Rank Fusion
|
||||
- **People intelligence**: Expert discovery, workload analysis, review patterns, active discussions, and code ownership overlap
|
||||
- **Timeline pipeline**: Reconstructs chronological event histories by combining search, graph traversal, and event aggregation across related entities
|
||||
@@ -17,6 +17,7 @@ Local GitLab data management with semantic search, people intelligence, and temp
|
||||
- **Raw payload storage**: Preserves original GitLab API responses for debugging
|
||||
- **Discussion threading**: Full support for issue and MR discussions including inline code review comments
|
||||
- **Cross-reference tracking**: Automatic extraction of "closes", "mentioned" relationships between MRs and issues
|
||||
- **Work item status enrichment**: Fetches issue statuses (e.g., "To do", "In progress", "Done") from GitLab's GraphQL API with adaptive page sizing, color-coded display, and case-insensitive filtering
|
||||
- **Resource event history**: Tracks state changes, label events, and milestone events for issues and MRs
|
||||
- **Robot mode**: Machine-readable JSON output with structured errors, meaningful exit codes, and actionable recovery steps
|
||||
- **Observability**: Verbosity controls, JSON log format, structured metrics, and stage timing
|
||||
@@ -90,13 +91,15 @@ Configuration is stored in `~/.config/lore/config.json` (or `$XDG_CONFIG_HOME/lo
|
||||
{ "path": "group/project" },
|
||||
{ "path": "other-group/other-project" }
|
||||
],
|
||||
"defaultProject": "group/project",
|
||||
"sync": {
|
||||
"backfillDays": 14,
|
||||
"staleLockMinutes": 10,
|
||||
"heartbeatIntervalSeconds": 30,
|
||||
"cursorRewindSeconds": 2,
|
||||
"primaryConcurrency": 4,
|
||||
"dependentConcurrency": 2
|
||||
"dependentConcurrency": 2,
|
||||
"fetchWorkItemStatus": true
|
||||
},
|
||||
"storage": {
|
||||
"compressRawPayloads": true
|
||||
@@ -117,12 +120,14 @@ Configuration is stored in `~/.config/lore/config.json` (or `$XDG_CONFIG_HOME/lo
|
||||
| `gitlab` | `baseUrl` | -- | GitLab instance URL (required) |
|
||||
| `gitlab` | `tokenEnvVar` | `GITLAB_TOKEN` | Environment variable containing API token |
|
||||
| `projects` | `path` | -- | Project path (e.g., `group/project`) |
|
||||
| *(top-level)* | `defaultProject` | none | Fallback project path used when `-p` is omitted. Must match a configured project path (exact or suffix). CLI `-p` always overrides. |
|
||||
| `sync` | `backfillDays` | `14` | Days to backfill on initial sync |
|
||||
| `sync` | `staleLockMinutes` | `10` | Minutes before sync lock considered stale |
|
||||
| `sync` | `heartbeatIntervalSeconds` | `30` | Frequency of lock heartbeat updates |
|
||||
| `sync` | `cursorRewindSeconds` | `2` | Seconds to rewind cursor for overlap safety |
|
||||
| `sync` | `primaryConcurrency` | `4` | Concurrent GitLab requests for primary resources |
|
||||
| `sync` | `dependentConcurrency` | `2` | Concurrent requests for dependent resources |
|
||||
| `sync` | `fetchWorkItemStatus` | `true` | Enrich issues with work item status via GraphQL (requires GitLab Premium/Ultimate) |
|
||||
| `storage` | `dbPath` | `~/.local/share/lore/lore.db` | Database file path |
|
||||
| `storage` | `backupDir` | `~/.local/share/lore/backups` | Backup directory |
|
||||
| `storage` | `compressRawPayloads` | `true` | Compress stored API responses with gzip |
|
||||
@@ -184,6 +189,8 @@ lore issues --since 1m # Updated in last month
|
||||
lore issues --since 2024-01-01 # Updated since date
|
||||
lore issues --due-before 2024-12-31 # Due before date
|
||||
lore issues --has-due # Only issues with due dates
|
||||
lore issues --status "In progress" # By work item status (case-insensitive)
|
||||
lore issues --status "To do" --status "In progress" # Multiple statuses (OR)
|
||||
lore issues -p group/repo # Filter by project
|
||||
lore issues --sort created --asc # Sort by created date, ascending
|
||||
lore issues -o # Open first result in browser
|
||||
@@ -193,13 +200,13 @@ lore -J issues --fields minimal # Compact: iid, title, state, updated_at_i
|
||||
lore -J issues --fields iid,title,labels,state # Custom fields
|
||||
```
|
||||
|
||||
When listing, output includes: IID, title, state, author, assignee, labels, and update time. In robot mode, the `--fields` flag controls which fields appear in the JSON response.
|
||||
When listing, output includes: IID, title, state, status (when any issue has one), assignee, labels, and update time. Status values display with their configured color. In robot mode, the `--fields` flag controls which fields appear in the JSON response.
|
||||
|
||||
When showing a single issue (e.g., `lore issues 123`), output includes: title, description, state, author, assignees, labels, milestone, due date, web URL, and threaded discussions.
|
||||
When showing a single issue (e.g., `lore issues 123`), output includes: title, description, state, work item status (with color and category), author, assignees, labels, milestone, due date, web URL, and threaded discussions.
|
||||
|
||||
#### Project Resolution
|
||||
|
||||
The `-p` / `--project` flag uses cascading match logic across all commands:
|
||||
When `-p` / `--project` is omitted, the `defaultProject` from config is used as a fallback. If neither is set, results span all configured projects. When a project is specified (via `-p` or config default), it uses cascading match logic across all commands:
|
||||
|
||||
1. **Exact match**: `group/project`
|
||||
2. **Case-insensitive**: `Group/Project`
|
||||
@@ -397,7 +404,7 @@ When graph expansion encounters cross-project references to entities not yet syn
|
||||
|
||||
### `lore sync`
|
||||
|
||||
Run the full sync pipeline: ingest from GitLab, generate searchable documents, and compute embeddings.
|
||||
Run the full sync pipeline: ingest from GitLab (including work item status enrichment via GraphQL), generate searchable documents, and compute embeddings.
|
||||
|
||||
```bash
|
||||
lore sync # Full pipeline
|
||||
@@ -413,11 +420,11 @@ The sync command displays animated progress bars for each stage and outputs timi
|
||||
|
||||
### `lore ingest`
|
||||
|
||||
Sync data from GitLab to local database. Runs only the ingestion step (no doc generation or embeddings).
|
||||
Sync data from GitLab to local database. Runs only the ingestion step (no doc generation or embeddings). For issue ingestion, this includes a status enrichment phase that fetches work item statuses via the GitLab GraphQL API.
|
||||
|
||||
```bash
|
||||
lore ingest # Ingest everything (issues + MRs)
|
||||
lore ingest issues # Issues only
|
||||
lore ingest issues # Issues only (includes status enrichment)
|
||||
lore ingest mrs # MRs only
|
||||
lore ingest issues -p group/repo # Single project
|
||||
lore ingest --force # Override stale lock
|
||||
@@ -430,6 +437,8 @@ The `--full` flag resets sync cursors and discussion watermarks, then fetches al
|
||||
- You want to ensure complete data after schema changes
|
||||
- Troubleshooting sync issues
|
||||
|
||||
Status enrichment uses adaptive page sizing (100 → 50 → 25 → 10) to handle GitLab GraphQL complexity limits. It gracefully handles instances without GraphQL support or Premium/Ultimate licensing. Disable via `sync.fetchWorkItemStatus: false` in config.
|
||||
|
||||
### `lore generate-docs`
|
||||
|
||||
Extract searchable documents from ingested issues, MRs, and discussions for the FTS5 index.
|
||||
@@ -501,12 +510,15 @@ lore init --force # Overwrite existing config
|
||||
lore init --non-interactive # Fail if prompts needed
|
||||
```
|
||||
|
||||
When multiple projects are configured, `init` prompts whether to set a default project (used when `-p` is omitted). This can also be set via the `--default-project` flag.
|
||||
|
||||
In robot mode, `init` supports non-interactive setup via flags:
|
||||
|
||||
```bash
|
||||
lore -J init --gitlab-url https://gitlab.com \
|
||||
--token-env-var GITLAB_TOKEN \
|
||||
--projects "group/project,other/project"
|
||||
--projects "group/project,other/project" \
|
||||
--default-project group/project
|
||||
```
|
||||
|
||||
### `lore auth`
|
||||
@@ -623,7 +635,7 @@ lore -J issues --fields iid,title,state,labels,updated_at_iso
|
||||
# minimal: iid, title, state, updated_at_iso
|
||||
```
|
||||
|
||||
Valid fields for issues: `iid`, `title`, `state`, `author_username`, `labels`, `assignees`, `discussion_count`, `unresolved_count`, `created_at_iso`, `updated_at_iso`, `web_url`, `project_path`
|
||||
Valid fields for issues: `iid`, `title`, `state`, `author_username`, `labels`, `assignees`, `discussion_count`, `unresolved_count`, `created_at_iso`, `updated_at_iso`, `web_url`, `project_path`, `status_name`, `status_category`, `status_color`, `status_icon_name`, `status_synced_at_iso`
|
||||
|
||||
Valid fields for MRs: `iid`, `title`, `state`, `author_username`, `labels`, `draft`, `target_branch`, `source_branch`, `discussion_count`, `unresolved_count`, `created_at_iso`, `updated_at_iso`, `web_url`, `project_path`, `reviewers`
|
||||
|
||||
@@ -714,7 +726,7 @@ Data is stored in SQLite with WAL mode and foreign keys enabled. Main tables:
|
||||
| Table | Purpose |
|
||||
|-------|---------|
|
||||
| `projects` | Tracked GitLab projects with metadata |
|
||||
| `issues` | Issue metadata (title, state, author, due date, milestone) |
|
||||
| `issues` | Issue metadata (title, state, author, due date, milestone, work item status) |
|
||||
| `merge_requests` | MR metadata (title, state, draft, branches, merge status, commit SHAs) |
|
||||
| `milestones` | Project milestones with state and due dates |
|
||||
| `labels` | Project labels with colors |
|
||||
|
||||
245
docs/diagrams/01-human-flow-map.excalidraw
Normal file
245
docs/diagrams/01-human-flow-map.excalidraw
Normal file
@@ -0,0 +1,245 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{ "type": "text", "id": "title", "x": 300, "y": 15, "text": "Human User Flow Map", "fontSize": 28 },
|
||||
{ "type": "text", "id": "subtitle", "x": 220, "y": 53, "text": "15 human workflows mapped to lore commands. Arrows show data dependency.", "fontSize": 14, "strokeColor": "#868e96" },
|
||||
|
||||
{ "type": "text", "id": "col-trigger", "x": 60, "y": 80, "text": "TRIGGER (Problem)", "fontSize": 16, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "col-flow", "x": 400, "y": 80, "text": "COMMAND FLOW", "fontSize": 16, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "col-gap", "x": 880, "y": 80, "text": "GAP", "fontSize": 16, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-daily", "x": 20, "y": 110, "width": 960, "height": 190,
|
||||
"backgroundColor": "#dbe4ff", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#4a9eed", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-daily-label", "x": 30, "y": 115, "text": "Daily Operations", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
|
||||
{ "type": "rectangle", "id": "h1-trigger", "x": 30, "y": 140, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H1: Standup prep\n\"What moved overnight?\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h1-a1", "x": 230, "y": 165, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h1-cmd1", "x": 280, "y": 145, "width": 90, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "sync -q", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h1-a2", "x": 370, "y": 165, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h1-cmd2", "x": 400, "y": 145, "width": 140, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "issues --since 1d", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h1-a3", "x": 540, "y": 165, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h1-cmd3", "x": 570, "y": 145, "width": 130, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "mrs --since 1d", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h1-a4", "x": 700, "y": 165, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h1-cmd4", "x": 730, "y": 145, "width": 100, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "who @me", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h1-a5", "x": 830, "y": 165, "width": 40, "height": 0,
|
||||
"points": [[0,0],[40,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h1-gap", "x": 870, "y": 140, "width": 100, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No @me\nNo feed", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "h3-trigger", "x": 30, "y": 210, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H3: Incident\n\"Deploy broke prod\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h3-a1", "x": 230, "y": 235, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h3-cmd1", "x": 280, "y": 215, "width": 130, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "timeline deploy", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h3-a2", "x": 410, "y": 235, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h3-cmd2", "x": 440, "y": 215, "width": 160, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "search deploy --mr", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h3-a3", "x": 600, "y": 235, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h3-cmd3", "x": 630, "y": 215, "width": 110, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "mrs <iid>", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h3-a4", "x": 740, "y": 235, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h3-cmd4", "x": 770, "y": 215, "width": 100, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "who --overlap", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-planning", "x": 20, "y": 310, "width": 960, "height": 190,
|
||||
"backgroundColor": "#d3f9d8", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#22c55e", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-planning-label", "x": 30, "y": 315, "text": "Planning & Assignment", "fontSize": 14, "strokeColor": "#15803d" },
|
||||
|
||||
{ "type": "rectangle", "id": "h2-trigger", "x": 30, "y": 340, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H2: Sprint plan\n\"What's ready to pick?\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h2-a1", "x": 230, "y": 365, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h2-cmd1", "x": 280, "y": 345, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "issues -s opened -l ready", "fontSize": 13 } },
|
||||
{ "type": "arrow", "id": "h2-a2", "x": 450, "y": 365, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h2-cmd2", "x": 480, "y": 345, "width": 150, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "issues --has-due", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h2-a3", "x": 630, "y": 365, "width": 230, "height": 0,
|
||||
"points": [[0,0],[230,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h2-gap", "x": 860, "y": 340, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No\n--no-assignee", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "h8-trigger", "x": 30, "y": 410, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H8: Assign work\n\"Who has bandwidth?\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h8-a1", "x": 230, "y": 435, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h8-cmd1", "x": 280, "y": 415, "width": 120, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "who @alice", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h8-a2", "x": 400, "y": 435, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h8-cmd2", "x": 430, "y": 415, "width": 110, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "who @bob", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h8-a3", "x": 540, "y": 435, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h8-cmd3", "x": 570, "y": 415, "width": 120, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "who @carol...", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h8-a4", "x": 690, "y": 435, "width": 170, "height": 0,
|
||||
"points": [[0,0],[170,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h8-gap", "x": 860, "y": 410, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No team\nworkload view", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-investigation", "x": 20, "y": 510, "width": 960, "height": 260,
|
||||
"backgroundColor": "#fff3bf", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#f59e0b", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-invest-label", "x": 30, "y": 515, "text": "Investigation & Understanding", "fontSize": 14, "strokeColor": "#b45309" },
|
||||
|
||||
{ "type": "rectangle", "id": "h7-trigger", "x": 30, "y": 540, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H7: Why this way?\n\"Understand a decision\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h7-a1", "x": 230, "y": 565, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h7-cmd1", "x": 280, "y": 545, "width": 160, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "search \"rationale\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h7-a2", "x": 440, "y": 565, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h7-cmd2", "x": 470, "y": 545, "width": 140, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "timeline --depth 2", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h7-a3", "x": 610, "y": 565, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h7-cmd3", "x": 640, "y": 545, "width": 100, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "issues 234", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h7-a4", "x": 740, "y": 565, "width": 120, "height": 0,
|
||||
"points": [[0,0],[120,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h7-gap", "x": 860, "y": 540, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No per-note\nsearch", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "h11-trigger", "x": 30, "y": 610, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H11: Bug lifecycle\n\"Why does #321 reopen?\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h11-a1", "x": 230, "y": 635, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h11-cmd1", "x": 280, "y": 615, "width": 120, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "issues 321", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h11-a2", "x": 400, "y": 635, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h11-cmd2", "x": 430, "y": 615, "width": 130, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "timeline ???", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h11-a3", "x": 560, "y": 635, "width": 300, "height": 0,
|
||||
"points": [[0,0],[300,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h11-gap", "x": 860, "y": 610, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No entity\ntimeline", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "h14-trigger", "x": 30, "y": 680, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H14: Prior art?\n\"Was this tried before?\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h14-a1", "x": 230, "y": 705, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h14-cmd1", "x": 280, "y": 685, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "search \"memory leak\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h14-a2", "x": 450, "y": 705, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h14-cmd2", "x": 480, "y": 685, "width": 120, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "mrs --closed?", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h14-a3", "x": 600, "y": 705, "width": 260, "height": 0,
|
||||
"points": [[0,0],[260,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h14-gap", "x": 860, "y": 680, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No --state\non search", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-people", "x": 20, "y": 780, "width": 960, "height": 190,
|
||||
"backgroundColor": "#e5dbff", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#8b5cf6", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-people-label", "x": 30, "y": 785, "text": "People & Expertise", "fontSize": 14, "strokeColor": "#7048e8" },
|
||||
|
||||
{ "type": "rectangle", "id": "h4-trigger", "x": 30, "y": 810, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H4: Review prep\n\"Context for MR !789\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h4-a1", "x": 230, "y": 835, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h4-cmd1", "x": 280, "y": 815, "width": 100, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "mrs 789", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h4-a2", "x": 380, "y": 835, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h4-cmd2", "x": 410, "y": 815, "width": 120, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "who src/auth/", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h4-a3", "x": 530, "y": 835, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h4-cmd3", "x": 560, "y": 815, "width": 130, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "search \"auth\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h4-a4", "x": 690, "y": 835, "width": 170, "height": 0,
|
||||
"points": [[0,0],[170,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h4-gap", "x": 860, "y": 810, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No MR file\nlist output", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "h6-trigger", "x": 30, "y": 880, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "H6: Find reviewer\n\"Who should review?\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h6-a1", "x": 230, "y": 905, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h6-cmd1", "x": 280, "y": 885, "width": 130, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "who src/auth/", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h6-a2", "x": 410, "y": 905, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h6-cmd2", "x": 440, "y": 885, "width": 140, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "who src/pay/", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h6-a3", "x": 580, "y": 905, "width": 30, "height": 0,
|
||||
"points": [[0,0],[30,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h6-cmd3", "x": 610, "y": 885, "width": 140, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "who @candidate", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "h6-a4", "x": 750, "y": 905, "width": 110, "height": 0,
|
||||
"points": [[0,0],[110,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "h6-gap", "x": 860, "y": 880, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No multi-\npath query", "fontSize": 14 } },
|
||||
|
||||
{ "type": "text", "id": "callout-1", "x": 30, "y": 990, "text": "Pattern: Most human flows require 3-5 serial commands. Average gap rate: 73% of flows have at least one.", "fontSize": 14, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "callout-2", "x": 30, "y": 1015, "text": "Top optimization: Composite commands (activity feed, team workload) would reduce multi-command flows by ~40%.", "fontSize": 14, "strokeColor": "#15803d" },
|
||||
{ "type": "text", "id": "callout-3", "x": 30, "y": 1040, "text": "Top missing data: MR file changes and entity references are stored but invisible to CLI users.", "fontSize": 14, "strokeColor": "#ef4444" }
|
||||
],
|
||||
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
|
||||
"files": {}
|
||||
}
|
||||
BIN
docs/diagrams/01-human-flow-map.png
Normal file
BIN
docs/diagrams/01-human-flow-map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 274 KiB |
204
docs/diagrams/02-agent-flow-map.excalidraw
Normal file
204
docs/diagrams/02-agent-flow-map.excalidraw
Normal file
@@ -0,0 +1,204 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{ "type": "text", "id": "title", "x": 320, "y": 15, "text": "AI Agent Flow Map", "fontSize": 28 },
|
||||
{ "type": "text", "id": "subtitle", "x": 180, "y": 53, "text": "15 agent automation workflows. Agents need structured JSON (-J), exit codes, and field selection.", "fontSize": 14, "strokeColor": "#868e96" },
|
||||
|
||||
{ "type": "text", "id": "col-trigger", "x": 60, "y": 80, "text": "TRIGGER (Agent Goal)", "fontSize": 16, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "col-flow", "x": 400, "y": 80, "text": "COMMAND PIPELINE", "fontSize": 16, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "col-gap", "x": 880, "y": 80, "text": "BLOCKED BY", "fontSize": 16, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-context", "x": 20, "y": 110, "width": 960, "height": 200,
|
||||
"backgroundColor": "#e5dbff", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#8b5cf6", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-context-label", "x": 30, "y": 115, "text": "Context Gathering (pre-action)", "fontSize": 14, "strokeColor": "#7048e8" },
|
||||
|
||||
{ "type": "rectangle", "id": "a1-trigger", "x": 30, "y": 140, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "A1: Pre-edit context\nAbout to modify files", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a1-a1", "x": 230, "y": 165, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a1-cmd1", "x": 280, "y": 145, "width": 80, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J health", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a1-a2", "x": 360, "y": 165, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a1-cmd2", "x": 380, "y": 145, "width": 140, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J who src/auth/", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a1-a3", "x": 520, "y": 165, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a1-cmd3", "x": 540, "y": 145, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J search \"auth\" -n 10", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a1-a4", "x": 710, "y": 165, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a1-cmd4", "x": 730, "y": 145, "width": 130, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J who --overlap", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "a6-trigger", "x": 30, "y": 210, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "A6: Auto-assign reviewers\nBased on file expertise", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a6-a1", "x": 230, "y": 235, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a6-cmd1", "x": 280, "y": 215, "width": 100, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "-J mrs 456", "fontSize": 14 } },
|
||||
{ "type": "text", "id": "a6-block", "x": 390, "y": 218, "text": "file list not\nin response!", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
{ "type": "arrow", "id": "a6-a2", "x": 380, "y": 245, "width": 480, "height": -10,
|
||||
"points": [[0,0],[480,-10]], "endArrowhead": "arrow", "strokeColor": "#ef4444", "strokeStyle": "dashed" },
|
||||
{ "type": "rectangle", "id": "a6-gap", "x": 860, "y": 210, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "MR files\nnot exposed", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-report", "x": 20, "y": 320, "width": 960, "height": 200,
|
||||
"backgroundColor": "#d3f9d8", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#22c55e", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-report-label", "x": 30, "y": 325, "text": "Reporting & Synthesis", "fontSize": 14, "strokeColor": "#15803d" },
|
||||
|
||||
{ "type": "rectangle", "id": "a3-trigger", "x": 30, "y": 350, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "A3: Sprint status report\n7 queries for 1 report", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a3-a1", "x": 230, "y": 375, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a3-cmd1", "x": 280, "y": 352, "width": 100, "height": 36,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "issues -s closed", "fontSize": 12 } },
|
||||
{ "type": "rectangle", "id": "a3-cmd2", "x": 390, "y": 352, "width": 100, "height": 36,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "issues --status", "fontSize": 12 } },
|
||||
{ "type": "rectangle", "id": "a3-cmd3", "x": 500, "y": 352, "width": 100, "height": 36,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "mrs -s merged", "fontSize": 12 } },
|
||||
{ "type": "rectangle", "id": "a3-cmd4", "x": 610, "y": 352, "width": 80, "height": 36,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "mrs -s open", "fontSize": 12 } },
|
||||
{ "type": "rectangle", "id": "a3-cmd5", "x": 700, "y": 352, "width": 80, "height": 36,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "count x2", "fontSize": 12 } },
|
||||
{ "type": "rectangle", "id": "a3-cmd6", "x": 790, "y": 352, "width": 60, "height": 36,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "who", "fontSize": 12 } },
|
||||
{ "type": "arrow", "id": "a3-agap", "x": 850, "y": 370, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a3-gap", "x": 860, "y": 350, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No summary\ncommand", "fontSize": 14 } },
|
||||
{ "type": "text", "id": "a3-note", "x": 280, "y": 395, "text": "7 sequential API calls for one report. A `lore summary` could reduce to 1.", "fontSize": 12, "strokeColor": "#868e96" },
|
||||
|
||||
{ "type": "rectangle", "id": "a7-trigger", "x": 30, "y": 430, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "A7: Incident timeline\nPostmortem reconstruction", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a7-a1", "x": 230, "y": 455, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a7-cmd1", "x": 280, "y": 435, "width": 190, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J timeline --depth 2", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a7-a2", "x": 470, "y": 455, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a7-cmd2", "x": 490, "y": 435, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J search --since 3d", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a7-a3", "x": 660, "y": 455, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a7-cmd3", "x": 680, "y": 435, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J mrs -s merged", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-discover", "x": 20, "y": 530, "width": 960, "height": 200,
|
||||
"backgroundColor": "#fff3bf", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#f59e0b", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-discover-label", "x": 30, "y": 535, "text": "Discovery & Correlation", "fontSize": 14, "strokeColor": "#b45309" },
|
||||
|
||||
{ "type": "rectangle", "id": "a5-trigger", "x": 30, "y": 560, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "A5: PR description\nFind related issues to link", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a5-a1", "x": 230, "y": 585, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a5-cmd1", "x": 280, "y": 565, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J search keywords", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a5-a2", "x": 450, "y": 585, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a5-cmd2", "x": 470, "y": 565, "width": 180, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J issues --fields iid,url", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a5-a3", "x": 650, "y": 585, "width": 210, "height": 0,
|
||||
"points": [[0,0],[210,0]], "endArrowhead": "arrow", "strokeColor": "#ef4444", "strokeStyle": "dashed" },
|
||||
{ "type": "rectangle", "id": "a5-gap", "x": 860, "y": 560, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No refs\nquery", "fontSize": 14 } },
|
||||
{ "type": "text", "id": "a5-note", "x": 280, "y": 612, "text": "Agent can't ask \"which issues does MR !456 close?\" -- entity_references data exists but isn't queryable.", "fontSize": 12, "strokeColor": "#868e96" },
|
||||
|
||||
{ "type": "rectangle", "id": "a11-trigger", "x": 30, "y": 640, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "A11: Knowledge graph\nMap entity relationships", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a11-a1", "x": 230, "y": 665, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a11-cmd1", "x": 280, "y": 645, "width": 140, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J search -n 30", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a11-a2", "x": 420, "y": 665, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a11-cmd2", "x": 440, "y": 645, "width": 190, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J timeline --depth 2", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a11-a3", "x": 630, "y": 665, "width": 230, "height": 0,
|
||||
"points": [[0,0],[230,0]], "endArrowhead": "arrow", "strokeColor": "#ef4444", "strokeStyle": "dashed" },
|
||||
{ "type": "rectangle", "id": "a11-gap", "x": 860, "y": 640, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No refs\nquery", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-maint", "x": 20, "y": 740, "width": 960, "height": 140,
|
||||
"backgroundColor": "#dbe4ff", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#4a9eed", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-maint-label", "x": 30, "y": 745, "text": "Maintenance & Cleanup", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
|
||||
{ "type": "rectangle", "id": "a9-trigger", "x": 30, "y": 770, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "A9: Stale issue cleanup\nWeekly backlog hygiene", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a9-a1", "x": 230, "y": 795, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a9-cmd1", "x": 280, "y": 775, "width": 200, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J issues --sort updated --asc", "fontSize": 12 } },
|
||||
{ "type": "arrow", "id": "a9-a2", "x": 480, "y": 795, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a9-cmd2", "x": 500, "y": 775, "width": 120, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "filter client-side", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a9-a3", "x": 620, "y": 795, "width": 240, "height": 0,
|
||||
"points": [[0,0],[240,0]], "endArrowhead": "arrow", "strokeColor": "#ef4444", "strokeStyle": "dashed" },
|
||||
{ "type": "rectangle", "id": "a9-gap", "x": 860, "y": 770, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No --before\nNo offset", "fontSize": 14 } },
|
||||
|
||||
{ "type": "rectangle", "id": "a15-trigger", "x": 30, "y": 840, "width": 200, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "A15: Conflict detect\n\"Safe to start work?\"", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a15-a1", "x": 230, "y": 865, "width": 50, "height": 0,
|
||||
"points": [[0,0],[50,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a15-cmd1", "x": 280, "y": 845, "width": 110, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J issues 123", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a15-a2", "x": 390, "y": 865, "width": 20, "height": 0,
|
||||
"points": [[0,0],[20,0]], "endArrowhead": "arrow" },
|
||||
{ "type": "rectangle", "id": "a15-cmd2", "x": 410, "y": 845, "width": 130, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "-J who --overlap", "fontSize": 14 } },
|
||||
{ "type": "arrow", "id": "a15-a3", "x": 540, "y": 865, "width": 320, "height": 0,
|
||||
"points": [[0,0],[320,0]], "endArrowhead": "arrow", "strokeColor": "#ef4444", "strokeStyle": "dashed" },
|
||||
{ "type": "rectangle", "id": "a15-gap", "x": 860, "y": 840, "width": 110, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "No refs +\n--state", "fontSize": 14 } },
|
||||
|
||||
{ "type": "text", "id": "callout-1", "x": 30, "y": 910, "text": "Agent-specific pain: Agents always use -J and --fields minimal for token efficiency. Every extra query burns tokens.", "fontSize": 14, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "callout-2", "x": 30, "y": 935, "text": "Biggest ROI: `lore refs` command would unblock A5, A11, A12, A15 instantly. Data already exists in entity_references table.", "fontSize": 14, "strokeColor": "#15803d" },
|
||||
{ "type": "text", "id": "callout-3", "x": 30, "y": 960, "text": "Token waste: Sprint report (A3) requires 7 calls. A composite `lore summary` could save ~85% of tokens.", "fontSize": 14, "strokeColor": "#ef4444" }
|
||||
],
|
||||
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
|
||||
"files": {}
|
||||
}
|
||||
BIN
docs/diagrams/02-agent-flow-map.png
Normal file
BIN
docs/diagrams/02-agent-flow-map.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 269 KiB |
203
docs/diagrams/03-command-coverage.excalidraw
Normal file
203
docs/diagrams/03-command-coverage.excalidraw
Normal file
@@ -0,0 +1,203 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{ "type": "text", "id": "title", "x": 280, "y": 15, "text": "Command Coverage Heatmap", "fontSize": 28 },
|
||||
{ "type": "text", "id": "subtitle", "x": 220, "y": 53, "text": "Which commands serve which workflows? Darker = more essential to that flow.", "fontSize": 14, "strokeColor": "#868e96" },
|
||||
|
||||
{ "type": "text", "id": "col-issues", "x": 260, "y": 85, "text": "issues", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "col-mrs", "x": 330, "y": 85, "text": "mrs", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "col-search", "x": 390, "y": 85, "text": "search", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "col-who", "x": 465, "y": 85, "text": "who", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "col-timeline", "x": 520, "y": 85, "text": "timeline", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "col-sync", "x": 600, "y": 85, "text": "sync", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "col-count", "x": 660, "y": 85, "text": "count", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "col-status", "x": 720, "y": 85, "text": "status", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "col-missing", "x": 790, "y": 85, "text": "MISSING?", "fontSize": 14, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "grp-human", "x": 15, "y": 108, "text": "HUMAN FLOWS", "fontSize": 14, "strokeColor": "#15803d" },
|
||||
|
||||
{ "type": "text", "id": "h1-label", "x": 15, "y": 135, "text": "H1 Standup prep", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h1-issues", "x": 255, "y": 130, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h1-mrs", "x": 325, "y": 130, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h1-who", "x": 460, "y": 130, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h1-sync", "x": 595, "y": 130, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h1-gap", "x": 780, "y": 135, "text": "activity feed", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h2-label", "x": 15, "y": 170, "text": "H2 Sprint planning", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h2-issues", "x": 255, "y": 165, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h2-count", "x": 655, "y": 165, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h2-gap", "x": 780, "y": 170, "text": "--no-assignee", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h3-label", "x": 15, "y": 205, "text": "H3 Incident response", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h3-mrs", "x": 325, "y": 200, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h3-search", "x": 390, "y": 200, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h3-who", "x": 460, "y": 200, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h3-timeline", "x": 525, "y": 200, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h3-sync", "x": 595, "y": 200, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
|
||||
{ "type": "text", "id": "h4-label", "x": 15, "y": 240, "text": "H4 Code review prep", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h4-mrs", "x": 325, "y": 235, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h4-search", "x": 390, "y": 235, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h4-who", "x": 460, "y": 235, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h4-timeline", "x": 525, "y": 235, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h4-gap", "x": 780, "y": 240, "text": "MR file list", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h5-label", "x": 15, "y": 275, "text": "H5 Onboarding", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h5-issues", "x": 255, "y": 270, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h5-mrs", "x": 325, "y": 270, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h5-search", "x": 390, "y": 270, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h5-who", "x": 460, "y": 270, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h5-timeline", "x": 525, "y": 270, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
|
||||
{ "type": "text", "id": "h6-label", "x": 15, "y": 310, "text": "H6 Find reviewer", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h6-who", "x": 460, "y": 305, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h6-gap", "x": 780, "y": 310, "text": "multi-path who", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h7-label", "x": 15, "y": 345, "text": "H7 Why was this built?", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h7-issues", "x": 255, "y": 340, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h7-mrs", "x": 325, "y": 340, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h7-search", "x": 390, "y": 340, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h7-timeline", "x": 525, "y": 340, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h7-gap", "x": 780, "y": 345, "text": "per-note search", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h8-label", "x": 15, "y": 380, "text": "H8 Team workload", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h8-who", "x": 460, "y": 375, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h8-gap", "x": 780, "y": 380, "text": "team view", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h9-label", "x": 15, "y": 415, "text": "H9 Release notes", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h9-issues", "x": 255, "y": 410, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h9-mrs", "x": 325, "y": 410, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h9-gap", "x": 780, "y": 415, "text": "mrs --milestone", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h10-label", "x": 15, "y": 450, "text": "H10 Stale issues", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h10-issues", "x": 255, "y": 445, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h10-gap", "x": 780, "y": 450, "text": "--updated-before", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h11-label", "x": 15, "y": 485, "text": "H11 Bug lifecycle", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h11-issues", "x": 255, "y": 480, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h11-timeline", "x": 525, "y": 480, "width": 50, "height": 28, "backgroundColor": "#ffd8a8", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h11-gap", "x": 780, "y": 485, "text": "entity timeline", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h12-label", "x": 15, "y": 520, "text": "H12 Who broke tests?", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h12-search", "x": 390, "y": 515, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h12-who", "x": 460, "y": 515, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
|
||||
{ "type": "text", "id": "h13-label", "x": 15, "y": 555, "text": "H13 Feature tracking", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h13-issues", "x": 255, "y": 550, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h13-mrs", "x": 325, "y": 550, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h13-timeline", "x": 525, "y": 550, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
|
||||
{ "type": "text", "id": "h14-label", "x": 15, "y": 590, "text": "H14 Prior art check", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h14-search", "x": 390, "y": 585, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "h14-timeline", "x": 525, "y": 585, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h14-gap", "x": 780, "y": 590, "text": "--state on search", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "h15-label", "x": 15, "y": 625, "text": "H15 My discussions", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "h15-who", "x": 460, "y": 620, "width": 50, "height": 28, "backgroundColor": "#ffd8a8", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "h15-gap", "x": 780, "y": 625, "text": "participant filter", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "rectangle", "id": "divider", "x": 10, "y": 655, "width": 910, "height": 2, "backgroundColor": "#dee2e6", "fillStyle": "solid" },
|
||||
|
||||
{ "type": "text", "id": "grp-agent", "x": 15, "y": 668, "text": "AI AGENT FLOWS", "fontSize": 14, "strokeColor": "#7048e8" },
|
||||
|
||||
{ "type": "text", "id": "a1-label", "x": 15, "y": 695, "text": "A1 Pre-edit context", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a1-mrs", "x": 325, "y": 690, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a1-search", "x": 390, "y": 690, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a1-who", "x": 460, "y": 690, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
|
||||
{ "type": "text", "id": "a2-label", "x": 15, "y": 730, "text": "A2 Auto-triage", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a2-issues", "x": 255, "y": 725, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a2-search", "x": 390, "y": 725, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a2-who", "x": 460, "y": 725, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a2-gap", "x": 780, "y": 730, "text": "detail --fields", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a3-label", "x": 15, "y": 765, "text": "A3 Sprint report", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a3-issues", "x": 255, "y": 760, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a3-mrs", "x": 325, "y": 760, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a3-who", "x": 460, "y": 760, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a3-count", "x": 655, "y": 760, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a3-gap", "x": 780, "y": 765, "text": "summary cmd", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a4-label", "x": 15, "y": 800, "text": "A4 Prior art", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a4-search", "x": 390, "y": 795, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a4-timeline", "x": 525, "y": 795, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a4-gap", "x": 780, "y": 800, "text": "per-note search", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a5-label", "x": 15, "y": 835, "text": "A5 PR description", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a5-issues", "x": 255, "y": 830, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a5-search", "x": 390, "y": 830, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a5-gap", "x": 780, "y": 835, "text": "entity refs query", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a6-label", "x": 15, "y": 870, "text": "A6 Reviewer assign", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a6-mrs", "x": 325, "y": 865, "width": 50, "height": 28, "backgroundColor": "#ffd8a8", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a6-who", "x": 460, "y": 865, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a6-gap", "x": 780, "y": 870, "text": "MR file list", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a7-label", "x": 15, "y": 905, "text": "A7 Incident timeline", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a7-mrs", "x": 325, "y": 900, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a7-search", "x": 390, "y": 900, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a7-timeline", "x": 525, "y": 900, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
|
||||
{ "type": "text", "id": "a8-label", "x": 15, "y": 940, "text": "A8 Cross-project", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a8-search", "x": 390, "y": 935, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a8-timeline", "x": 525, "y": 935, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a8-gap", "x": 780, "y": 940, "text": "group by project", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a9-label", "x": 15, "y": 975, "text": "A9 Stale cleanup", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a9-issues", "x": 255, "y": 970, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a9-search", "x": 390, "y": 970, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a9-gap", "x": 780, "y": 975, "text": "--updated-before", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a10-label", "x": 15, "y": 1010, "text": "A10 Review context", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a10-mrs", "x": 325, "y": 1005, "width": 50, "height": 28, "backgroundColor": "#ffd8a8", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a10-who", "x": 460, "y": 1005, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a10-gap", "x": 780, "y": 1010, "text": "MR file list", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a11-label", "x": 15, "y": 1045, "text": "A11 Knowledge graph", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a11-search", "x": 390, "y": 1040, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a11-timeline", "x": 525, "y": 1040, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a11-gap", "x": 780, "y": 1045, "text": "entity refs query", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a12-label", "x": 15, "y": 1080, "text": "A12 Release check", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a12-issues", "x": 255, "y": 1075, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a12-mrs", "x": 325, "y": 1075, "width": 50, "height": 28, "backgroundColor": "#ffd8a8", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a12-who", "x": 460, "y": 1075, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a12-gap", "x": 780, "y": 1080, "text": "mrs --milestone", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a13-label", "x": 15, "y": 1115, "text": "A13 What changed?", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a13-issues", "x": 255, "y": 1110, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a13-mrs", "x": 325, "y": 1110, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a13-gap", "x": 780, "y": 1115, "text": "state-change filter", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a14-label", "x": 15, "y": 1150, "text": "A14 Meeting prep", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a14-issues", "x": 255, "y": 1145, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a14-mrs", "x": 325, "y": 1145, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a14-who", "x": 460, "y": 1145, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a14-count", "x": 655, "y": 1145, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a14-gap", "x": 780, "y": 1150, "text": "summary cmd", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "a15-label", "x": 15, "y": 1185, "text": "A15 Conflict detect", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "a15-issues", "x": 255, "y": 1180, "width": 50, "height": 28, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a15-mrs", "x": 325, "y": 1180, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "rectangle", "id": "a15-who", "x": 460, "y": 1180, "width": 50, "height": 28, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "a15-gap", "x": 780, "y": 1185, "text": "entity refs, --state", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "legend-title", "x": 15, "y": 1230, "text": "Legend:", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "leg-essential", "x": 80, "y": 1228, "width": 20, "height": 20, "backgroundColor": "#22c55e", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "leg-essential-t", "x": 105, "y": 1230, "text": "Essential", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "leg-supporting", "x": 190, "y": 1228, "width": 20, "height": 20, "backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "leg-supporting-t", "x": 215, "y": 1230, "text": "Supporting", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "leg-partial", "x": 310, "y": 1228, "width": 20, "height": 20, "backgroundColor": "#ffd8a8", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "leg-partial-t", "x": 335, "y": 1230, "text": "Partially blocked", "fontSize": 14 },
|
||||
{ "type": "text", "id": "leg-gap-t", "x": 470, "y": 1230, "text": "Red text = gap", "fontSize": 14, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "text", "id": "insight-1", "x": 15, "y": 1270, "text": "Key insight: `issues` and `search` are the workhorses (used in 20+ flows).", "fontSize": 14, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "insight-2", "x": 15, "y": 1295, "text": "`who` is critical for people questions but siloed from file-change data.", "fontSize": 14, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "insight-3", "x": 15, "y": 1320, "text": "`timeline` is powerful but keyword-only seeding limits entity-specific queries.", "fontSize": 14, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "insight-4", "x": 15, "y": 1345, "text": "22/30 flows have at least one gap. Most gaps are filter additions, not new commands.", "fontSize": 14, "strokeColor": "#ef4444" }
|
||||
],
|
||||
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
|
||||
"files": {}
|
||||
}
|
||||
BIN
docs/diagrams/03-command-coverage.png
Normal file
BIN
docs/diagrams/03-command-coverage.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 217 KiB |
110
docs/diagrams/04-gap-priority-matrix.excalidraw
Normal file
110
docs/diagrams/04-gap-priority-matrix.excalidraw
Normal file
@@ -0,0 +1,110 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{ "type": "text", "id": "title", "x": 300, "y": 20, "text": "Lore CLI Gap Priority Matrix", "fontSize": 28 },
|
||||
{ "type": "text", "id": "subtitle", "x": 310, "y": 58, "text": "20 identified gaps plotted by impact vs effort", "fontSize": 16, "strokeColor": "#868e96" },
|
||||
|
||||
{ "type": "rectangle", "id": "q1-zone", "x": 100, "y": 120, "width": 500, "height": 380,
|
||||
"backgroundColor": "#d3f9d8", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#22c55e", "strokeWidth": 1, "opacity": 25 },
|
||||
{ "type": "text", "id": "q1-label", "x": 110, "y": 126, "text": "QUICK WINS", "fontSize": 18, "strokeColor": "#15803d" },
|
||||
|
||||
{ "type": "rectangle", "id": "q2-zone", "x": 620, "y": 120, "width": 500, "height": 380,
|
||||
"backgroundColor": "#fff3bf", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#f59e0b", "strokeWidth": 1, "opacity": 25 },
|
||||
{ "type": "text", "id": "q2-label", "x": 630, "y": 126, "text": "STRATEGIC", "fontSize": 18, "strokeColor": "#b45309" },
|
||||
|
||||
{ "type": "rectangle", "id": "q3-zone", "x": 100, "y": 520, "width": 500, "height": 300,
|
||||
"backgroundColor": "#dbe4ff", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#4a9eed", "strokeWidth": 1, "opacity": 25 },
|
||||
{ "type": "text", "id": "q3-label", "x": 110, "y": 526, "text": "FILL-IN", "fontSize": 18, "strokeColor": "#1971c2" },
|
||||
|
||||
{ "type": "rectangle", "id": "q4-zone", "x": 620, "y": 520, "width": 500, "height": 300,
|
||||
"backgroundColor": "#ffc9c9", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#ef4444", "strokeWidth": 1, "opacity": 25 },
|
||||
{ "type": "text", "id": "q4-label", "x": 630, "y": 526, "text": "DEPRIORITIZE", "fontSize": 18, "strokeColor": "#c92a2a" },
|
||||
|
||||
{ "type": "text", "id": "y-axis-hi", "x": 30, "y": 130, "text": "HIGH\nIMPACT", "fontSize": 16, "strokeColor": "#495057", "textAlign": "center" },
|
||||
{ "type": "text", "id": "y-axis-lo", "x": 30, "y": 550, "text": "LOW\nIMPACT", "fontSize": 16, "strokeColor": "#495057", "textAlign": "center" },
|
||||
{ "type": "text", "id": "x-axis-lo", "x": 280, "y": 840, "text": "LOW EFFORT", "fontSize": 16, "strokeColor": "#495057" },
|
||||
{ "type": "text", "id": "x-axis-hi", "x": 800, "y": 840, "text": "HIGH EFFORT", "fontSize": 16, "strokeColor": "#495057" },
|
||||
|
||||
{ "type": "arrow", "id": "y-arrow", "x": 85, "y": 810, "width": 0, "height": -680,
|
||||
"points": [[0,0],[0,-680]], "endArrowhead": "arrow", "strokeColor": "#495057", "strokeWidth": 1 },
|
||||
{ "type": "arrow", "id": "x-arrow", "x": 85, "y": 810, "width": 1050, "height": 0,
|
||||
"points": [[0,0],[1050,0]], "endArrowhead": "arrow", "strokeColor": "#495057", "strokeWidth": 1 },
|
||||
|
||||
{ "type": "rectangle", "id": "g5", "x": 120, "y": 160, "width": 210, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "#5 @me alias", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g8", "x": 120, "y": 225, "width": 210, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "#8 --state on search", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g9", "x": 120, "y": 290, "width": 210, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "#9 mrs --milestone", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g10", "x": 120, "y": 355, "width": 210, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "#10 --no-assignee", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g11", "x": 350, "y": 160, "width": 230, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "#11 --updated-before", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g14", "x": 350, "y": 225, "width": 230, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "#14 detail --fields", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g18", "x": 350, "y": 290, "width": 230, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "#18 1y/12m duration", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g20", "x": 350, "y": 355, "width": 230, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "#20 sort by due date", "fontSize": 16 } },
|
||||
|
||||
{ "type": "rectangle", "id": "g1", "x": 640, "y": 160, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "#1 MR file changes", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g2", "x": 640, "y": 225, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "#2 entity refs query", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g3", "x": 640, "y": 290, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "#3 per-note search", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g4", "x": 880, "y": 160, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "#4 entity timeline", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g6", "x": 880, "y": 225, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "#6 activity feed", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g12", "x": 880, "y": 290, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffd8a8", "fillStyle": "solid",
|
||||
"label": { "text": "#12 team workload", "fontSize": 16 } },
|
||||
|
||||
{ "type": "rectangle", "id": "g13", "x": 120, "y": 570, "width": 210, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "#13 pagination/offset", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g15", "x": 120, "y": 635, "width": 210, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "#15 group by project", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g19", "x": 120, "y": 700, "width": 210, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "#19 participant filter", "fontSize": 16 } },
|
||||
|
||||
{ "type": "rectangle", "id": "g7", "x": 640, "y": 570, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid",
|
||||
"label": { "text": "#7 multi-path who", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g16", "x": 640, "y": 635, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid",
|
||||
"label": { "text": "#16 trend metrics", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "g17", "x": 640, "y": 700, "width": 220, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid",
|
||||
"label": { "text": "#17 --for-issue on mrs", "fontSize": 16 } },
|
||||
|
||||
{ "type": "text", "id": "q1-count", "x": 180, "y": 430, "text": "8 gaps - lowest hanging fruit", "fontSize": 14, "strokeColor": "#15803d" },
|
||||
{ "type": "text", "id": "q2-count", "x": 710, "y": 370, "text": "6 gaps - build deliberately", "fontSize": 14, "strokeColor": "#b45309" },
|
||||
{ "type": "text", "id": "q3-count", "x": 160, "y": 770, "text": "3 gaps - fill as needed", "fontSize": 14, "strokeColor": "#1971c2" },
|
||||
{ "type": "text", "id": "q4-count", "x": 680, "y": 770, "text": "3 gaps - defer or rethink", "fontSize": 14, "strokeColor": "#c92a2a" }
|
||||
],
|
||||
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
|
||||
"files": {}
|
||||
}
|
||||
BIN
docs/diagrams/04-gap-priority-matrix.png
Normal file
BIN
docs/diagrams/04-gap-priority-matrix.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 179 KiB |
184
docs/diagrams/05-data-flow-architecture.excalidraw
Normal file
184
docs/diagrams/05-data-flow-architecture.excalidraw
Normal file
@@ -0,0 +1,184 @@
|
||||
{
|
||||
"type": "excalidraw",
|
||||
"version": 2,
|
||||
"source": "https://excalidraw.com",
|
||||
"elements": [
|
||||
{ "type": "text", "id": "title", "x": 350, "y": 15, "text": "Lore Data Flow Architecture", "fontSize": 28 },
|
||||
{ "type": "text", "id": "subtitle", "x": 280, "y": 53, "text": "Green = queryable via CLI | Red = stored but hidden | Gray = internal", "fontSize": 14, "strokeColor": "#868e96" },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-gitlab", "x": 30, "y": 90, "width": 200, "height": 300,
|
||||
"backgroundColor": "#e5dbff", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#8b5cf6", "strokeWidth": 1, "opacity": 30 },
|
||||
{ "type": "text", "id": "zone-gitlab-label", "x": 55, "y": 96, "text": "GitLab APIs", "fontSize": 16, "strokeColor": "#7048e8" },
|
||||
|
||||
{ "type": "rectangle", "id": "rest-api", "x": 50, "y": 130, "width": 160, "height": 60,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "REST API\n(paginated)", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "graphql-api", "x": 50, "y": 210, "width": 160, "height": 60,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "GraphQL API\n(adaptive pages)", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "ollama-api", "x": 50, "y": 310, "width": 160, "height": 60,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#d0bfff", "fillStyle": "solid",
|
||||
"label": { "text": "Ollama\n(embeddings)", "fontSize": 16 } },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-ingest", "x": 270, "y": 90, "width": 180, "height": 300,
|
||||
"backgroundColor": "#dbe4ff", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#4a9eed", "strokeWidth": 1, "opacity": 30 },
|
||||
{ "type": "text", "id": "zone-ingest-label", "x": 300, "y": 96, "text": "Ingestion", "fontSize": 16, "strokeColor": "#1971c2" },
|
||||
|
||||
{ "type": "rectangle", "id": "ingest-issues", "x": 285, "y": 130, "width": 150, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "Issue Sync", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "ingest-mrs", "x": 285, "y": 195, "width": 150, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "MR Sync", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "ingest-disc", "x": 285, "y": 260, "width": 150, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "Discussion Sync", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "ingest-events", "x": 285, "y": 325, "width": 150, "height": 50,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#a5d8ff", "fillStyle": "solid",
|
||||
"label": { "text": "Event Sync", "fontSize": 16 } },
|
||||
|
||||
{ "type": "arrow", "id": "a-rest-issues", "x": 210, "y": 155, "width": 75, "height": 0,
|
||||
"points": [[0,0],[75,0]], "endArrowhead": "arrow", "strokeColor": "#495057" },
|
||||
{ "type": "arrow", "id": "a-rest-mrs", "x": 210, "y": 165, "width": 75, "height": 50,
|
||||
"points": [[0,0],[75,50]], "endArrowhead": "arrow", "strokeColor": "#495057" },
|
||||
{ "type": "arrow", "id": "a-graphql-issues", "x": 210, "y": 240, "width": 75, "height": -80,
|
||||
"points": [[0,0],[75,-80]], "endArrowhead": "arrow", "strokeColor": "#495057" },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-sqlite", "x": 490, "y": 90, "width": 400, "height": 650,
|
||||
"backgroundColor": "#d3f9d8", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#22c55e", "strokeWidth": 1, "opacity": 20 },
|
||||
{ "type": "text", "id": "zone-sqlite-label", "x": 570, "y": 96, "text": "SQLite (WAL mode)", "fontSize": 16, "strokeColor": "#15803d" },
|
||||
|
||||
{ "type": "text", "id": "grp-queryable", "x": 500, "y": 120, "text": "Queryable Tables", "fontSize": 14, "strokeColor": "#15803d" },
|
||||
|
||||
{ "type": "rectangle", "id": "t-projects", "x": 500, "y": 145, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "projects", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-issues", "x": 500, "y": 195, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "issues + assignees", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-mrs", "x": 500, "y": 245, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "merge_requests", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-discussions", "x": 500, "y": 295, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "discussions + notes", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-events", "x": 500, "y": 345, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "resource_*_events", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-docs", "x": 500, "y": 395, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "documents + FTS5", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-embed", "x": 500, "y": 445, "width": 170, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#b2f2bb", "fillStyle": "solid",
|
||||
"label": { "text": "embeddings (vec)", "fontSize": 14 } },
|
||||
|
||||
{ "type": "text", "id": "grp-hidden", "x": 700, "y": 120, "text": "Hidden Tables", "fontSize": 14, "strokeColor": "#c92a2a" },
|
||||
|
||||
{ "type": "rectangle", "id": "t-file-changes", "x": 695, "y": 145, "width": 180, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "mr_file_changes", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-entity-refs", "x": 695, "y": 195, "width": 180, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "entity_references", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-raw", "x": 695, "y": 245, "width": 180, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444",
|
||||
"label": { "text": "raw_payloads", "fontSize": 14 } },
|
||||
|
||||
{ "type": "text", "id": "grp-internal", "x": 700, "y": 310, "text": "Internal Only", "fontSize": 14, "strokeColor": "#868e96" },
|
||||
|
||||
{ "type": "rectangle", "id": "t-sync", "x": 695, "y": 340, "width": 180, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#dee2e6", "fillStyle": "solid", "strokeColor": "#868e96",
|
||||
"label": { "text": "sync_runs + cursors", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-dirty", "x": 695, "y": 390, "width": 180, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#dee2e6", "fillStyle": "solid", "strokeColor": "#868e96",
|
||||
"label": { "text": "dirty_sources", "fontSize": 14 } },
|
||||
{ "type": "rectangle", "id": "t-locks", "x": 695, "y": 440, "width": 180, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#dee2e6", "fillStyle": "solid", "strokeColor": "#868e96",
|
||||
"label": { "text": "app_locks", "fontSize": 14 } },
|
||||
|
||||
{ "type": "arrow", "id": "a-ingest-tables", "x": 435, "y": 200, "width": 55, "height": 0,
|
||||
"points": [[0,0],[55,0]], "endArrowhead": "arrow", "strokeColor": "#495057" },
|
||||
|
||||
{ "type": "rectangle", "id": "zone-cli", "x": 930, "y": 90, "width": 250, "height": 650,
|
||||
"backgroundColor": "#fff3bf", "fillStyle": "solid", "roundness": { "type": 3 },
|
||||
"strokeColor": "#f59e0b", "strokeWidth": 1, "opacity": 25 },
|
||||
{ "type": "text", "id": "zone-cli-label", "x": 990, "y": 96, "text": "CLI Commands", "fontSize": 16, "strokeColor": "#b45309" },
|
||||
|
||||
{ "type": "rectangle", "id": "cmd-issues", "x": 950, "y": 130, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#fff3bf", "fillStyle": "solid",
|
||||
"label": { "text": "lore issues", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-mrs", "x": 950, "y": 185, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#fff3bf", "fillStyle": "solid",
|
||||
"label": { "text": "lore mrs", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-search", "x": 950, "y": 240, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#fff3bf", "fillStyle": "solid",
|
||||
"label": { "text": "lore search", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-who", "x": 950, "y": 295, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#fff3bf", "fillStyle": "solid",
|
||||
"label": { "text": "lore who", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-timeline", "x": 950, "y": 350, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#fff3bf", "fillStyle": "solid",
|
||||
"label": { "text": "lore timeline", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-count", "x": 950, "y": 405, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#fff3bf", "fillStyle": "solid",
|
||||
"label": { "text": "lore count", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-sync", "x": 950, "y": 460, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#fff3bf", "fillStyle": "solid",
|
||||
"label": { "text": "lore sync", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-status", "x": 950, "y": 515, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#fff3bf", "fillStyle": "solid",
|
||||
"label": { "text": "lore status", "fontSize": 16 } },
|
||||
|
||||
{ "type": "arrow", "id": "a-issues-cmd", "x": 670, "y": 215, "width": 270, "height": -65,
|
||||
"points": [[0,0],[270,-65]], "endArrowhead": "arrow", "strokeColor": "#22c55e", "strokeWidth": 2 },
|
||||
{ "type": "arrow", "id": "a-mrs-cmd", "x": 670, "y": 265, "width": 270, "height": -60,
|
||||
"points": [[0,0],[270,-60]], "endArrowhead": "arrow", "strokeColor": "#22c55e", "strokeWidth": 2 },
|
||||
{ "type": "arrow", "id": "a-docs-cmd", "x": 670, "y": 415, "width": 270, "height": -155,
|
||||
"points": [[0,0],[270,-155]], "endArrowhead": "arrow", "strokeColor": "#22c55e", "strokeWidth": 2 },
|
||||
{ "type": "arrow", "id": "a-embed-cmd", "x": 670, "y": 465, "width": 270, "height": -200,
|
||||
"points": [[0,0],[270,-200]], "endArrowhead": "arrow", "strokeColor": "#22c55e", "strokeWidth": 2 },
|
||||
{ "type": "arrow", "id": "a-events-cmd", "x": 670, "y": 365, "width": 270, "height": 5,
|
||||
"points": [[0,0],[270,5]], "endArrowhead": "arrow", "strokeColor": "#22c55e", "strokeWidth": 2 },
|
||||
|
||||
{ "type": "text", "id": "hidden-note-1", "x": 695, "y": 498, "text": "mr_file_changes: populated by\nMR sync but NOT queryable.\nBlocks H4, A6, A10 flows.", "fontSize": 14, "strokeColor": "#ef4444" },
|
||||
{ "type": "text", "id": "hidden-note-2", "x": 695, "y": 568, "text": "entity_references: used by\ntimeline internally but NOT\nqueryable. Blocks A5, A11.", "fontSize": 14, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "arrow", "id": "a-hidden-who", "x": 875, "y": 165, "width": 65, "height": 148,
|
||||
"points": [[0,0],[65,148]], "endArrowhead": "arrow", "strokeColor": "#ef4444", "strokeWidth": 2,
|
||||
"strokeStyle": "dashed" },
|
||||
{ "type": "text", "id": "hidden-who-label", "x": 880, "y": 240, "text": "who uses\nDiffNotes,\nnot file\nchanges", "fontSize": 12, "strokeColor": "#ef4444" },
|
||||
|
||||
{ "type": "arrow", "id": "a-hidden-timeline", "x": 875, "y": 215, "width": 65, "height": 155,
|
||||
"points": [[0,0],[65,155]], "endArrowhead": "arrow", "strokeColor": "#ef4444", "strokeWidth": 2,
|
||||
"strokeStyle": "dashed" },
|
||||
|
||||
{ "type": "rectangle", "id": "cmd-missing-refs", "x": 950, "y": 580, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444", "strokeStyle": "dashed",
|
||||
"label": { "text": "lore refs (missing)", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-missing-files", "x": 950, "y": 635, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444", "strokeStyle": "dashed",
|
||||
"label": { "text": "lore files (missing)", "fontSize": 16 } },
|
||||
{ "type": "rectangle", "id": "cmd-missing-activity", "x": 950, "y": 690, "width": 210, "height": 40,
|
||||
"roundness": { "type": 3 }, "backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444", "strokeStyle": "dashed",
|
||||
"label": { "text": "lore activity (missing)", "fontSize": 16 } },
|
||||
|
||||
{ "type": "text", "id": "legend-title", "x": 30, "y": 430, "text": "Legend", "fontSize": 16 },
|
||||
{ "type": "rectangle", "id": "leg-green", "x": 30, "y": 460, "width": 20, "height": 20,
|
||||
"backgroundColor": "#b2f2bb", "fillStyle": "solid" },
|
||||
{ "type": "text", "id": "leg-green-t", "x": 60, "y": 462, "text": "Queryable via CLI", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "leg-red", "x": 30, "y": 490, "width": 20, "height": 20,
|
||||
"backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444" },
|
||||
{ "type": "text", "id": "leg-red-t", "x": 60, "y": 492, "text": "Stored but hidden", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "leg-gray", "x": 30, "y": 520, "width": 20, "height": 20,
|
||||
"backgroundColor": "#dee2e6", "fillStyle": "solid", "strokeColor": "#868e96" },
|
||||
{ "type": "text", "id": "leg-gray-t", "x": 60, "y": 522, "text": "Internal bookkeeping", "fontSize": 14 },
|
||||
{ "type": "rectangle", "id": "leg-dashed", "x": 30, "y": 550, "width": 20, "height": 20,
|
||||
"backgroundColor": "#ffc9c9", "fillStyle": "solid", "strokeColor": "#ef4444", "strokeStyle": "dashed" },
|
||||
{ "type": "text", "id": "leg-dashed-t", "x": 60, "y": 552, "text": "Missing command", "fontSize": 14 }
|
||||
],
|
||||
"appState": { "viewBackgroundColor": "#ffffff", "gridSize": null },
|
||||
"files": {}
|
||||
}
|
||||
BIN
docs/diagrams/05-data-flow-architecture.png
Normal file
BIN
docs/diagrams/05-data-flow-architecture.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 238 KiB |
66
docs/ideas/README.md
Normal file
66
docs/ideas/README.md
Normal file
@@ -0,0 +1,66 @@
|
||||
# Gitlore Feature Ideas
|
||||
|
||||
Central registry of potential features. Each idea leverages data already ingested
|
||||
into the local SQLite database (issues, MRs, discussions, notes, resource events,
|
||||
entity references, embeddings, file changes).
|
||||
|
||||
## Priority Tiers
|
||||
|
||||
**Tier 1 — High confidence, low effort, immediate value:**
|
||||
|
||||
| # | Idea | File | Confidence |
|
||||
|---|------|------|------------|
|
||||
| 9 | Similar Issues Finder | [similar-issues.md](similar-issues.md) | 95% |
|
||||
| 17 | "What Changed?" Digest | [digest.md](digest.md) | 93% |
|
||||
| 5 | Who Knows About X? | [experts.md](experts.md) | 92% |
|
||||
| -- | Multi-Project Ergonomics | [project-ergonomics.md](project-ergonomics.md) | 90% |
|
||||
| 27 | Weekly Digest Generator | [weekly-digest.md](weekly-digest.md) | 90% |
|
||||
| 4 | Stale Discussion Finder | [stale-discussions.md](stale-discussions.md) | 90% |
|
||||
|
||||
**Tier 2 — Strong ideas, moderate effort:**
|
||||
|
||||
| # | Idea | File | Confidence |
|
||||
|---|------|------|------------|
|
||||
| 19 | MR-to-Issue Closure Gap | [closure-gaps.md](closure-gaps.md) | 88% |
|
||||
| 1 | Contributor Heatmap | [contributors.md](contributors.md) | 88% |
|
||||
| 21 | Knowledge Silo Detection | [silos.md](silos.md) | 87% |
|
||||
| 2 | Review Bottleneck Detector | [bottlenecks.md](bottlenecks.md) | 85% |
|
||||
| 14 | File Hotspot Report | [hotspots.md](hotspots.md) | 85% |
|
||||
| 26 | Unlinked MR Finder | [unlinked.md](unlinked.md) | 83% |
|
||||
| 6 | Decision Archaeology | [decisions.md](decisions.md) | 82% |
|
||||
| 18 | Label Hygiene Audit | [label-audit.md](label-audit.md) | 82% |
|
||||
|
||||
**Tier 3 — Promising, needs more design work:**
|
||||
|
||||
| # | Idea | File | Confidence |
|
||||
|---|------|------|------------|
|
||||
| 29 | Entity Relationship Explorer | [graph.md](graph.md) | 80% |
|
||||
| 12 | Milestone Risk Report | [milestone-risk.md](milestone-risk.md) | 78% |
|
||||
| 3 | Label Velocity | [label-flow.md](label-flow.md) | 78% |
|
||||
| 24 | Recurring Bug Patterns | [recurring-patterns.md](recurring-patterns.md) | 76% |
|
||||
| 7 | Cross-Project Impact Graph | [impact-graph.md](impact-graph.md) | 75% |
|
||||
| 16 | Idle Work Detector | [idle.md](idle.md) | 73% |
|
||||
| 8 | MR Churn Analysis | [churn.md](churn.md) | 72% |
|
||||
| 15 | Author Collaboration Network | [collaboration.md](collaboration.md) | 70% |
|
||||
| 28 | DiffNote Coverage Map | [review-coverage.md](review-coverage.md) | 75% |
|
||||
| 25 | MR Pipeline Efficiency | [mr-pipeline.md](mr-pipeline.md) | 78% |
|
||||
|
||||
## Rejected Ideas (with reasons)
|
||||
|
||||
| # | Idea | Reason |
|
||||
|---|------|--------|
|
||||
| 10 | Sprint Burndown from Labels | Too opinionated about label semantics |
|
||||
| 11 | Code Review Quality Score | Subjective "quality" scoring creates perverse incentives |
|
||||
| 13 | Discussion Sentiment Drift | Unreliable heuristic sentiment on technical text |
|
||||
| 20 | Response Time Leaderboard | Toxic "leaderboard" framing; metric folded into #2 |
|
||||
| 22 | Timeline Diff | Niche use case; timeline already interleaves events |
|
||||
| 23 | Discussion Thread Summarizer | Requires LLM inference; out of scope for local-first tool |
|
||||
| 30 | NL Query Interface | Over-engineered; existing filters cover this |
|
||||
|
||||
## How to use this list
|
||||
|
||||
1. Pick an idea from Tier 1 or Tier 2
|
||||
2. Read its detail file for implementation plan and SQL sketches
|
||||
3. Create a bead (`br create`) referencing the idea file
|
||||
4. Implement following TDD (test first, then minimal impl)
|
||||
5. Update the idea file with `status: implemented` when done
|
||||
555
docs/ideas/SYSTEM-PROPOSAL.md
Normal file
555
docs/ideas/SYSTEM-PROPOSAL.md
Normal file
@@ -0,0 +1,555 @@
|
||||
# Project Manager System — Design Proposal
|
||||
|
||||
## The Problem
|
||||
|
||||
We have a growing backlog of ideas and issues in markdown files. Agents can ship
|
||||
features in under an hour. The constraint isn't execution speed — it's knowing
|
||||
WHAT to execute NEXT, in what ORDER, and detecting when the plan needs to change.
|
||||
|
||||
We need a system that:
|
||||
1. Automatically scores and sequences work items
|
||||
2. Detects when scope changes during spec generation
|
||||
3. Tracks the full lifecycle: idea → spec → beads → shipped
|
||||
4. Re-triages instantly when the dependency graph changes
|
||||
5. Runs in seconds, not minutes
|
||||
|
||||
## Architecture
|
||||
|
||||
```
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ docs/ideas/*.md │
|
||||
│ docs/issues/*.md │
|
||||
│ (YAML frontmatter) │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ IDEA TRIAGE SKILL │
|
||||
│ │
|
||||
│ Phase 1: INGEST — parse all frontmatter │
|
||||
│ Phase 2: VALIDATE — check refs, detect staleness │
|
||||
│ Phase 3: EVALUATE — detect scope changes since last run │
|
||||
│ Phase 4: SCORE — compute priority with unlock graph │
|
||||
│ Phase 5: SEQUENCE — topological sort by dependency + score │
|
||||
│ Phase 6: RECOMMEND — top 3 + unlock advisories + warnings │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ HUMAN DECIDES │
|
||||
│ (picks from top 3, takes seconds) │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ SPEC GENERATION (Claude/GPT) │
|
||||
│ Takes the idea doc, generates detailed implementation spec │
|
||||
│ ALSO: re-evaluates frontmatter fields based on deeper │
|
||||
│ understanding. Updates effort, blocked-by, components. │
|
||||
│ This is the SCOPE CHANGE DETECTION point. │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ PLAN-TO-BEADS (existing skill) │
|
||||
│ Spec → granular beads with dependencies via br CLI │
|
||||
│ Links bead IDs back into the idea frontmatter │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ AGENT IMPLEMENTATION │
|
||||
│ Works beads via br/bv workflow │
|
||||
│ bv --robot-triage handles execution-phase prioritization │
|
||||
└──────────────────────────┬──────────────────────────────────┘
|
||||
│
|
||||
▼
|
||||
┌─────────────────────────────────────────────────────────────┐
|
||||
│ COMPLETION & RE-TRIAGE │
|
||||
│ Beads close → idea status updates to implemented │
|
||||
│ Skill re-runs → newly unblocked ideas surface │
|
||||
│ Loop back to top │
|
||||
└─────────────────────────────────────────────────────────────┘
|
||||
```
|
||||
|
||||
## The Two Systems and Their Boundary
|
||||
|
||||
| Concern | Ideas System (new) | Beads System (existing) |
|
||||
|---------|-------------------|------------------------|
|
||||
| Phase | Pre-commitment (what to build) | Execution (how to build) |
|
||||
| Data | docs/ideas/*.md, docs/issues/*.md | .beads/issues.jsonl |
|
||||
| Triage | Idea triage skill | bv --robot-triage |
|
||||
| Tracking | YAML frontmatter | JSONL records |
|
||||
| Granularity | Feature-level | Task-level |
|
||||
| Lifecycle | proposed → specced → promoted | open → in_progress → closed |
|
||||
|
||||
**The handoff point is promotion.** An idea becomes one or more beads. After that,
|
||||
the ideas system only tracks the idea's status (promoted/implemented). Beads owns
|
||||
execution.
|
||||
|
||||
An idea file is NEVER deleted. It's a permanent design record. Even after
|
||||
implementation, it documents WHY the feature was built and what tradeoffs were made.
|
||||
|
||||
---
|
||||
|
||||
## Data Model
|
||||
|
||||
### Frontmatter Schema
|
||||
|
||||
```yaml
|
||||
---
|
||||
# ── Identity ──
|
||||
id: idea-009 # stable unique identifier
|
||||
title: Similar Issues Finder
|
||||
type: idea # idea | issue
|
||||
status: proposed # see lifecycle below
|
||||
|
||||
# ── Timestamps ──
|
||||
created: 2026-02-09
|
||||
updated: 2026-02-09
|
||||
eval-hash: null # SHA of scoring fields at last triage run
|
||||
|
||||
# ── Scoring Inputs ──
|
||||
impact: high # high | medium | low
|
||||
effort: small # small | medium | large | xlarge
|
||||
severity: null # critical | high | medium | low (issues only)
|
||||
autonomy: full # full | needs-design | needs-human
|
||||
|
||||
# ── Dependency Graph ──
|
||||
blocked-by: [] # IDs of ideas/issues that must complete first
|
||||
unlocks: # IDs that become possible/better after this ships
|
||||
- idea-recurring-patterns
|
||||
requires: [] # external prerequisites (gate names)
|
||||
related: # soft links, not blocking
|
||||
- issue-001
|
||||
|
||||
# ── Implementation Context ──
|
||||
components: # source code paths this will touch
|
||||
- src/search/
|
||||
- src/embedding/
|
||||
command: lore similar # proposed CLI command (null for issues)
|
||||
has-spec: false # detailed spec has been generated
|
||||
spec-path: null # path to spec doc if it exists
|
||||
beads: [] # bead IDs after promotion
|
||||
|
||||
# ── Classification ──
|
||||
tags:
|
||||
- embeddings
|
||||
- search
|
||||
---
|
||||
```
|
||||
|
||||
### Status Lifecycle
|
||||
|
||||
```
|
||||
IDEA lifecycle:
|
||||
proposed ──→ accepted ──→ specced ──→ promoted ──→ implemented
|
||||
│ │
|
||||
└──→ rejected └──→ (scope changed, back to accepted)
|
||||
|
||||
ISSUE lifecycle:
|
||||
open ──→ accepted ──→ specced ──→ promoted ──→ resolved
|
||||
│
|
||||
└──→ wontfix
|
||||
```
|
||||
|
||||
Transitions:
|
||||
- `proposed → accepted`: Human confirms this is worth building
|
||||
- `accepted → specced`: Detailed implementation spec has been generated
|
||||
- `specced → promoted`: Beads created from the spec
|
||||
- `promoted → implemented`: All beads closed
|
||||
- Any → `rejected`/`wontfix`: Decided not to build (with reason in body)
|
||||
- `specced → accepted`: Scope changed during spec, needs re-evaluation
|
||||
|
||||
### Effort Calibration (Agent-Executed)
|
||||
|
||||
| Level | Wall Clock | Autonomy | Example |
|
||||
|-------|-----------|----------|---------|
|
||||
| small | ~30 min | Agent ships end-to-end | stale-discussions, closure-gaps |
|
||||
| medium | ~1 hour | Agent ships end-to-end | similar-issues, digest |
|
||||
| large | 1-2 hours | May need one design decision | recurring-patterns, experts |
|
||||
| xlarge | 2+ hours | Needs human architecture input | project groups |
|
||||
|
||||
### Gates Registry (docs/gates.yaml)
|
||||
|
||||
```yaml
|
||||
gates:
|
||||
gate-1:
|
||||
title: Resource Events Ingestion
|
||||
status: complete
|
||||
completed: 2025-12-15
|
||||
|
||||
gate-2:
|
||||
title: Cross-References & Entity Graph
|
||||
status: complete
|
||||
completed: 2026-01-10
|
||||
|
||||
gate-3:
|
||||
title: Timeline Pipeline
|
||||
status: complete
|
||||
completed: 2026-01-25
|
||||
|
||||
gate-4:
|
||||
title: MR File Changes Ingestion
|
||||
status: partial
|
||||
notes: Schema ready (migration 016), ingestion code exists but untested
|
||||
tracks: mr_file_changes table population
|
||||
|
||||
gate-5:
|
||||
title: Code Trace (file:line → commit → MR → issue)
|
||||
status: not-started
|
||||
blocked-by: gate-4
|
||||
notes: Requires git log parsing + commit SHA matching
|
||||
```
|
||||
|
||||
The skill reads this file to determine which `requires` entries are satisfied.
|
||||
|
||||
---
|
||||
|
||||
## Scoring Algorithm
|
||||
|
||||
### Priority Score
|
||||
|
||||
```
|
||||
For ideas:
|
||||
base = impact_weight # high=3, medium=2, low=1
|
||||
unlock = 1 + (0.5 × count_of_unlocks) # items this directly enables
|
||||
readiness = 0 if blocked, 1 if ready
|
||||
priority = base × unlock × readiness
|
||||
|
||||
For issues:
|
||||
base = severity_weight × 1.5 # critical=6, high=4.5, medium=3, low=1.5
|
||||
unlock = 1 + (0.5 × count_of_unlocks) # (bugs rarely unlock, but can)
|
||||
readiness = 0 if blocked, 1 if ready
|
||||
priority = base × unlock × readiness
|
||||
|
||||
Tiebreak (among equal priority):
|
||||
1. Prefer smaller effort (ships faster, starts next cycle sooner)
|
||||
2. Prefer autonomy:full over needs-design over needs-human
|
||||
3. Prefer older items (FIFO within same score)
|
||||
```
|
||||
|
||||
### Why This Works
|
||||
|
||||
- High-impact items that unlock other items float to the top
|
||||
- Blocked items score 0 regardless of impact (can't be worked)
|
||||
- Effort is a tiebreaker, not a primary factor (since execution is fast)
|
||||
- Issues with severity get a 1.5× multiplier (bugs degrade existing value)
|
||||
- Unlock multiplier captures the "do Gate 4 first" insight automatically
|
||||
|
||||
### Example Rankings
|
||||
|
||||
| Item | Impact | Unlocks | Readiness | Score |
|
||||
|------|--------|---------|-----------|-------|
|
||||
| project-ergonomics | high(3) | 10 | ready(1) | 3 × 6.0 = 18.0 |
|
||||
| gate-4-completion | med(2) | 5 | ready(1) | 2 × 3.5 = 7.0 |
|
||||
| similar-issues | high(3) | 1 | ready(1) | 3 × 1.5 = 4.5 |
|
||||
| stale-discussions | high(3) | 0 | ready(1) | 3 × 1.0 = 3.0 |
|
||||
| hotspots | high(3) | 1 | blocked(0) | 0.0 |
|
||||
|
||||
Project-ergonomics dominates because it unlocks 10 downstream items. This is the
|
||||
correct recommendation — it's the highest-leverage work even though "stale-discussions"
|
||||
is simpler.
|
||||
|
||||
---
|
||||
|
||||
## Scope Change Detection
|
||||
|
||||
This is the hardest problem. An idea's scope can change in three ways:
|
||||
|
||||
### 1. During Spec Generation (Primary Detection Point)
|
||||
|
||||
When Claude/GPT generates a detailed implementation spec from an idea doc, it
|
||||
understands the idea more deeply than the original sketch. The spec process should
|
||||
be instructed to:
|
||||
|
||||
- Re-evaluate effort (now that implementation is understood in detail)
|
||||
- Discover new dependencies (need to change schema first, need a new config option)
|
||||
- Identify component changes (touches more modules than originally thought)
|
||||
- Assess impact more accurately (this is actually higher/lower value than estimated)
|
||||
|
||||
**Mechanism:** The spec generation prompt includes an explicit "re-evaluate frontmatter"
|
||||
step. The spec output includes an updated frontmatter block. If scoring-relevant
|
||||
fields changed, the skill flags it:
|
||||
|
||||
```
|
||||
SCOPE CHANGE DETECTED:
|
||||
idea-009 (Similar Issues Finder)
|
||||
- effort: small → medium (needs embedding aggregation strategy)
|
||||
- blocked-by: [] → [gate-embeddings-populated]
|
||||
- components: +src/cli/commands/similar.rs (new file)
|
||||
Previous score: 4.5 → New score: 3.0
|
||||
Recommendation: Still top-3, but sequencing may change.
|
||||
```
|
||||
|
||||
### 2. During Implementation (Discovered Complexity)
|
||||
|
||||
An agent working on beads may discover the spec was wrong:
|
||||
- "This requires a database migration I didn't anticipate"
|
||||
- "This module doesn't expose the API I need"
|
||||
|
||||
**Mechanism:** When a bead is blocked or takes significantly longer than estimated,
|
||||
the agent should update the idea's frontmatter. The skill detects the change on
|
||||
next triage run via eval-hash comparison.
|
||||
|
||||
### 3. External Changes (Gate Completion, New Ideas)
|
||||
|
||||
When a gate completes or a new idea is added that changes the dependency graph:
|
||||
- Gate 4 completes → 5 ideas become unblocked
|
||||
- New idea added that's higher priority than current top-3
|
||||
- Two ideas discovered to be duplicates
|
||||
|
||||
**Mechanism:** The skill detects these automatically by re-computing the full graph
|
||||
on every run. The eval-hash tracks what the scoring fields looked like last time;
|
||||
if they haven't changed but the SCORE changed (because a dependency was resolved),
|
||||
the skill flags it as "newly unblocked."
|
||||
|
||||
### The eval-hash Field
|
||||
|
||||
```yaml
|
||||
eval-hash: "a1b2c3d4" # SHA-256 of: impact + effort + blocked-by + unlocks + requires
|
||||
```
|
||||
|
||||
Computed by hashing the concatenation of all scoring-relevant fields. When the skill
|
||||
runs, it compares:
|
||||
- If eval-hash matches AND score is same → no change, skip
|
||||
- If eval-hash matches BUT score changed → external change (dependency resolved)
|
||||
- If eval-hash differs → item was modified, re-evaluate
|
||||
|
||||
This avoids re-announcing unchanged items on every run.
|
||||
|
||||
---
|
||||
|
||||
## Skill Design
|
||||
|
||||
### Location
|
||||
|
||||
`.claude/skills/idea-triage/SKILL.md` (project-local)
|
||||
|
||||
### Trigger Phrases
|
||||
|
||||
- "triage ideas" / "what should I build next?"
|
||||
- "idea triage" / "prioritize ideas"
|
||||
- "what's the highest value work?"
|
||||
- `/idea-triage`
|
||||
|
||||
### Workflow Phases
|
||||
|
||||
**Phase 1: INGEST**
|
||||
- Glob docs/ideas/*.md and docs/issues/*.md
|
||||
- Parse YAML frontmatter from each file
|
||||
- Read docs/gates.yaml for capability status
|
||||
- Collect: id, title, type, status, impact, effort, severity, autonomy,
|
||||
blocked-by, unlocks, requires, has-spec, beads, eval-hash
|
||||
|
||||
**Phase 2: VALIDATE**
|
||||
- Required fields present (id, title, type, status, impact, effort)
|
||||
- All blocked-by IDs reference existing files
|
||||
- All unlocks IDs reference existing files
|
||||
- All requires entries exist in gates.yaml
|
||||
- No dependency cycles (blocked-by graph is a DAG)
|
||||
- Status transitions are valid (no "proposed" with beads linked)
|
||||
- Output: list of validation errors/warnings
|
||||
|
||||
**Phase 3: EVALUATE (Scope Change Detection)**
|
||||
- For each item, compute current eval-hash from scoring fields
|
||||
- Compare against stored eval-hash in frontmatter
|
||||
- If different: flag as SCOPE_CHANGED with field-level diff
|
||||
- If same but score changed (due to external dep resolution): flag as NEWLY_UNBLOCKED
|
||||
- If status is specced but has-spec is false: flag as INCONSISTENT
|
||||
|
||||
**Phase 4: SCORE**
|
||||
- Resolve requires against gates.yaml (is the gate complete?)
|
||||
- Resolve blocked-by against other items (is the blocker done?)
|
||||
- Compute readiness: 0 if any hard blocker is unresolved, 1 otherwise
|
||||
- Compute unlock count: count items whose blocked-by includes this ID
|
||||
- Apply scoring formula:
|
||||
- Ideas: impact_weight × (1 + 0.5 × unlock_count) × readiness
|
||||
- Issues: severity_weight × 1.5 × (1 + 0.5 × unlock_count) × readiness
|
||||
- Apply tiebreak: effort_weight, autonomy, created date
|
||||
|
||||
**Phase 5: SEQUENCE**
|
||||
- Separate into: actionable (score > 0) vs blocked (score = 0)
|
||||
- Among actionable: sort by score descending with tiebreak
|
||||
- Among blocked: sort by "what-if score" (score if blockers were resolved)
|
||||
- Compute unlock advisories: "completing X unblocks Y items worth Z total score"
|
||||
|
||||
**Phase 6: RECOMMEND**
|
||||
Output structured report:
|
||||
|
||||
```
|
||||
== IDEA TRIAGE ==
|
||||
Run: 2026-02-09T14:30:00Z
|
||||
Items: 22 (18 proposed, 2 accepted, 1 specced, 1 implemented)
|
||||
|
||||
RECOMMENDED SEQUENCE:
|
||||
1. [idea-project-ergonomics] Multi-Project Ergonomics
|
||||
impact:high effort:medium autonomy:full score:18.0
|
||||
WHY FIRST: Unlocks 10 downstream ideas. Highest leverage.
|
||||
COMPONENTS: src/core/config.rs, src/core/project.rs, src/cli/
|
||||
|
||||
2. [idea-009] Similar Issues Finder
|
||||
impact:high effort:small autonomy:full score:4.5
|
||||
WHY NEXT: Highest standalone impact. Ships in ~30 min.
|
||||
UNLOCKS: idea-recurring-patterns
|
||||
|
||||
3. [idea-004] Stale Discussion Finder
|
||||
impact:high effort:small autonomy:full score:3.0
|
||||
WHY NEXT: Quick win, no dependencies, immediate user value.
|
||||
|
||||
BLOCKED (would rank high if unblocked):
|
||||
idea-014 File Hotspots score-if-unblocked:4.5 BLOCKED BY: gate-4
|
||||
idea-021 Knowledge Silos score-if-unblocked:3.0 BLOCKED BY: gate-4
|
||||
UNLOCK ADVISORY: Completing gate-4 unblocks 5 items (combined: 15.0)
|
||||
|
||||
SCOPE CHANGES DETECTED:
|
||||
idea-009: effort changed small→medium (eval-hash mismatch)
|
||||
idea-017: now has spec (has-spec flipped to true)
|
||||
|
||||
NEWLY UNBLOCKED:
|
||||
(none this run)
|
||||
|
||||
WARNINGS:
|
||||
idea-016: status=proposed, unchanged for 30+ days
|
||||
idea-008: blocked-by references "idea-gate4" which doesn't exist (typo?)
|
||||
|
||||
HEALTH:
|
||||
Proposed: 18 | Accepted: 2 | Specced: 1 | Promoted: 0 | Implemented: 1
|
||||
Blocked: 6 | Actionable: 16
|
||||
Backlog runway at ~5/day: ~3 days
|
||||
```
|
||||
|
||||
### What the Skill Does NOT Do
|
||||
|
||||
- **Never modifies files.** Read-only triage. The agent or human updates frontmatter.
|
||||
Exception: the skill CAN update eval-hash after a triage run (opt-in).
|
||||
- **Never creates beads.** That's plan-to-beads skill territory.
|
||||
- **Never replaces bv.** Once work is in beads, bv --robot-triage handles execution
|
||||
prioritization. This skill owns pre-commitment only.
|
||||
- **Never generates specs.** That's a separate step with Claude/GPT.
|
||||
|
||||
---
|
||||
|
||||
## Integration Points
|
||||
|
||||
### With Spec Generation
|
||||
|
||||
The spec generation prompt (separate from this skill) should include:
|
||||
|
||||
```
|
||||
After generating the implementation spec, re-evaluate the idea's frontmatter:
|
||||
1. Is the effort estimate still accurate? (small/medium/large/xlarge)
|
||||
2. Did you discover new dependencies? (add to blocked-by)
|
||||
3. Are there components not listed? (add to components)
|
||||
4. Has the impact assessment changed?
|
||||
5. Can an agent ship this autonomously? (autonomy: full/needs-design/needs-human)
|
||||
|
||||
Output an UPDATED frontmatter block at the end of the spec.
|
||||
If any scoring field changed, explain what changed and why.
|
||||
```
|
||||
|
||||
### With plan-to-beads
|
||||
|
||||
When promoting an idea to beads:
|
||||
1. Run plan-to-beads on the spec
|
||||
2. Capture the created bead IDs
|
||||
3. Update the idea's frontmatter: status → promoted, beads → [bd-xxx, bd-yyy]
|
||||
4. Run br sync --flush-only && git add .beads/
|
||||
|
||||
### With bv --robot-triage
|
||||
|
||||
These systems don't talk to each other directly. The boundary is:
|
||||
- Idea triage skill → "build idea-009 next"
|
||||
- Human/agent generates spec → plan-to-beads → beads created
|
||||
- bv --robot-triage → "work on bd-xxx next"
|
||||
- Beads close → human/agent updates idea frontmatter → idea triage re-runs
|
||||
|
||||
### With New Item Ingestion
|
||||
|
||||
When someone adds a new file to docs/ideas/ or docs/issues/:
|
||||
- If it has valid frontmatter: picked up automatically on next triage run
|
||||
- If it has no/invalid frontmatter: flagged in WARNINGS section
|
||||
- Skill can suggest default frontmatter based on content analysis
|
||||
|
||||
---
|
||||
|
||||
## Failure Modes and Mitigations
|
||||
|
||||
### 1. Frontmatter Rot
|
||||
**Risk:** Fields don't get updated. Status says "proposed" but it's actually shipped.
|
||||
**Mitigation:** Cross-reference with beads. If an idea has beads and all beads are
|
||||
closed, flag that the idea should be "implemented" even if frontmatter says otherwise.
|
||||
The skill detects this inconsistency.
|
||||
|
||||
### 2. Score Gaming
|
||||
**Risk:** Someone inflates impact or unlocks count to make their idea rank higher.
|
||||
**Mitigation:** Unlocks are verified — the skill checks that the referenced items
|
||||
actually have this idea in their blocked-by. Impact is subjective but reviewed during
|
||||
spec generation (second opinion from a different model/session).
|
||||
|
||||
### 3. Stale Gates Registry
|
||||
**Risk:** gate-4 is actually complete but gates.yaml wasn't updated.
|
||||
**Mitigation:** Skill warns when a gate has been "partial" for a long time. Could
|
||||
also probe the codebase (check if mr_file_changes ingestion code exists and has tests).
|
||||
|
||||
### 4. Circular Dependencies
|
||||
**Risk:** A blocks B blocks A.
|
||||
**Mitigation:** Phase 2 validation explicitly checks for cycles in the blocked-by
|
||||
graph and reports them as errors.
|
||||
|
||||
### 5. Unlock Count Inflation
|
||||
**Risk:** An item claims to unlock 20 things, making it score astronomically.
|
||||
**Mitigation:** Unlock count is VERIFIED by checking reverse blocked-by references.
|
||||
If idea-X says it unlocks idea-Y, but idea-Y's blocked-by doesn't include idea-X,
|
||||
the claim is discounted. Both explicit unlocks and reverse blocked-by contribute to
|
||||
the count, but unverified claims are flagged.
|
||||
|
||||
### 6. Scope Creep During Spec
|
||||
**Risk:** Spec generation reveals the idea is actually 5× harder than estimated.
|
||||
The score drops, but the human has already mentally committed.
|
||||
**Mitigation:** The scope change detection makes this VISIBLE. The triage output
|
||||
explicitly shows "effort changed small→xlarge, score dropped from 4.5 to 0.75."
|
||||
Human can then decide: proceed anyway, or switch to a different top-3 pick.
|
||||
|
||||
### 7. Orphaned Ideas
|
||||
**Risk:** Ideas get promoted to beads, beads get implemented, but the idea file
|
||||
never gets updated. It sits in "promoted" forever.
|
||||
**Mitigation:** Skill checks: for each idea with status=promoted, look up the
|
||||
linked beads. If all beads are closed, flag: "idea-009 appears complete, update
|
||||
status to implemented."
|
||||
|
||||
---
|
||||
|
||||
## Implementation Plan
|
||||
|
||||
### Step 1: Create the Frontmatter Schema (this doc → applied to all files)
|
||||
- Define the exact YAML schema (above)
|
||||
- Create docs/gates.yaml
|
||||
- Apply frontmatter to all 22 existing files in docs/ideas/ and docs/issues/
|
||||
|
||||
### Step 2: Build the Skill
|
||||
- Create .claude/skills/idea-triage/SKILL.md
|
||||
- Implement all 6 phases in the skill prompt
|
||||
- The skill uses Glob, Read, and text processing — no external scripts needed
|
||||
(25 files is small enough for Claude to process directly)
|
||||
|
||||
### Step 3: Test the System
|
||||
- Run the skill against current files
|
||||
- Verify scoring matches manual expectations
|
||||
- Check that project-ergonomics ranks #1 (it should, due to unlock count)
|
||||
- Verify blocked items score 0
|
||||
- Check validation catches intentional errors
|
||||
|
||||
### Step 4: Run One Full Cycle
|
||||
- Pick the top recommendation
|
||||
- Generate a spec (separate session)
|
||||
- Verify scope change detection works (spec should update frontmatter)
|
||||
- Promote to beads via plan-to-beads
|
||||
- Implement
|
||||
- Verify completion detection works
|
||||
|
||||
### Step 5: Iterate
|
||||
- Run triage again after implementation
|
||||
- Verify newly unblocked items surface
|
||||
- Adjust scoring weights if rankings feel wrong
|
||||
- Add new ideas as they emerge
|
||||
88
docs/ideas/bottlenecks.md
Normal file
88
docs/ideas/bottlenecks.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Review Bottleneck Detector
|
||||
|
||||
- **Command:** `lore bottlenecks [--since <date>]`
|
||||
- **Confidence:** 85%
|
||||
- **Tier:** 2
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — join MRs with first review note, compute percentiles
|
||||
|
||||
## What
|
||||
|
||||
For MRs in a given time window, compute:
|
||||
1. **Time to first review** — created_at to first non-author DiffNote
|
||||
2. **Review cycles** — count of discussion resolution rounds
|
||||
3. **Time to merge** — created_at to merged_at
|
||||
|
||||
Flag MRs above P90 thresholds as bottlenecks.
|
||||
|
||||
## Why
|
||||
|
||||
Review bottlenecks are the #1 developer productivity killer. Making them visible
|
||||
and measurable is the first step to fixing them. This provides data for process
|
||||
retrospectives.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `merge_requests` (created_at, merged_at, author_username)
|
||||
- `notes` (note_type='DiffNote', author_username, created_at)
|
||||
- `discussions` (resolved, resolvable)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
-- Time to first review per MR
|
||||
SELECT
|
||||
mr.id,
|
||||
mr.iid,
|
||||
mr.title,
|
||||
mr.author_username,
|
||||
mr.created_at,
|
||||
mr.merged_at,
|
||||
p.path_with_namespace,
|
||||
MIN(n.created_at) as first_review_at,
|
||||
(MIN(n.created_at) - mr.created_at) / 3600000.0 as hours_to_first_review,
|
||||
(mr.merged_at - mr.created_at) / 3600000.0 as hours_to_merge
|
||||
FROM merge_requests mr
|
||||
JOIN projects p ON mr.project_id = p.id
|
||||
LEFT JOIN discussions d ON d.merge_request_id = mr.id
|
||||
LEFT JOIN notes n ON n.discussion_id = d.id
|
||||
AND n.note_type = 'DiffNote'
|
||||
AND n.is_system = 0
|
||||
AND n.author_username != mr.author_username
|
||||
WHERE mr.created_at >= ?1
|
||||
AND mr.state IN ('merged', 'opened')
|
||||
GROUP BY mr.id
|
||||
ORDER BY hours_to_first_review DESC NULLS FIRST;
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Review Bottlenecks (last 30 days)
|
||||
|
||||
P50 time to first review: 4.2h
|
||||
P90 time to first review: 28.1h
|
||||
P50 time to merge: 2.1d
|
||||
P90 time to merge: 8.3d
|
||||
|
||||
Slowest to review:
|
||||
!234 Refactor auth 72h to first review (alice, still open)
|
||||
!228 Database migration 48h to first review (bob, merged in 5d)
|
||||
|
||||
Most review cycles:
|
||||
!234 Refactor auth 8 discussion threads, 4 resolved
|
||||
!225 API versioning 6 discussion threads, 6 resolved
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Doesn't capture review done outside GitLab (Slack, in-person)
|
||||
- DiffNote timestamp != when reviewer started reading
|
||||
- Large MRs naturally take longer; no size normalization
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore bottlenecks --reviewer alice` — how fast does alice review?
|
||||
- Per-project comparison: which project has the fastest review cycle?
|
||||
- Trend line: is review speed improving or degrading over time?
|
||||
77
docs/ideas/churn.md
Normal file
77
docs/ideas/churn.md
Normal file
@@ -0,0 +1,77 @@
|
||||
# MR Churn Analysis
|
||||
|
||||
- **Command:** `lore churn [--since <date>]`
|
||||
- **Confidence:** 72%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — multi-table aggregation with composite scoring
|
||||
|
||||
## What
|
||||
|
||||
For merged MRs, compute a "contentiousness score" based on: number of review
|
||||
discussions, number of DiffNotes, resolution cycles, file count. Flag high-churn
|
||||
MRs as candidates for architectural review.
|
||||
|
||||
## Why
|
||||
|
||||
High-churn MRs often indicate architectural disagreements, unclear requirements,
|
||||
or code that's hard to review. Surfacing them post-merge enables retrospectives
|
||||
and identifies areas that need better design upfront.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `merge_requests` (state='merged')
|
||||
- `discussions` (merge_request_id, resolved, resolvable)
|
||||
- `notes` (note_type='DiffNote', discussion_id)
|
||||
- `mr_file_changes` (file count per MR)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
mr.iid,
|
||||
mr.title,
|
||||
mr.author_username,
|
||||
p.path_with_namespace,
|
||||
COUNT(DISTINCT d.id) as discussion_count,
|
||||
COUNT(DISTINCT CASE WHEN n.note_type = 'DiffNote' THEN n.id END) as diffnote_count,
|
||||
COUNT(DISTINCT CASE WHEN d.resolvable = 1 AND d.resolved = 1 THEN d.id END) as resolved_threads,
|
||||
COUNT(DISTINCT mfc.id) as files_changed,
|
||||
-- Composite score: normalize each metric and weight
|
||||
(COUNT(DISTINCT d.id) * 2 + COUNT(DISTINCT n.id) + COUNT(DISTINCT mfc.id)) as churn_score
|
||||
FROM merge_requests mr
|
||||
JOIN projects p ON mr.project_id = p.id
|
||||
LEFT JOIN discussions d ON d.merge_request_id = mr.id AND d.noteable_type = 'MergeRequest'
|
||||
LEFT JOIN notes n ON n.discussion_id = d.id AND n.is_system = 0
|
||||
LEFT JOIN mr_file_changes mfc ON mfc.merge_request_id = mr.id
|
||||
WHERE mr.state = 'merged'
|
||||
AND mr.merged_at >= ?1
|
||||
GROUP BY mr.id
|
||||
ORDER BY churn_score DESC
|
||||
LIMIT ?2;
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
High-Churn MRs (last 90 days)
|
||||
|
||||
MR Discussions DiffNotes Files Score Title
|
||||
!234 12 28 8 60 Refactor auth middleware
|
||||
!225 8 19 5 39 API versioning v2
|
||||
!218 6 15 12 39 Database schema migration
|
||||
!210 5 8 3 21 Update logging framework
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- High discussion count could mean thorough review, not contention
|
||||
- Composite scoring weights are arbitrary; needs calibration per team
|
||||
- Large MRs naturally score higher regardless of contention
|
||||
|
||||
## Extensions
|
||||
|
||||
- Normalize by file count (discussions per file changed)
|
||||
- Compare against team averages (flag outliers, not absolute values)
|
||||
- `lore churn --author alice` — which of alice's MRs generate the most discussion?
|
||||
73
docs/ideas/closure-gaps.md
Normal file
73
docs/ideas/closure-gaps.md
Normal file
@@ -0,0 +1,73 @@
|
||||
# MR-to-Issue Closure Gap
|
||||
|
||||
- **Command:** `lore closure-gaps`
|
||||
- **Confidence:** 88%
|
||||
- **Tier:** 2
|
||||
- **Status:** proposed
|
||||
- **Effort:** low — single join query
|
||||
|
||||
## What
|
||||
|
||||
Find entity_references where reference_type='closes' AND the target issue is still
|
||||
open AND the source MR is merged. These represent broken auto-close links where a
|
||||
merge should have closed an issue but didn't.
|
||||
|
||||
## Why
|
||||
|
||||
Simple, definitive, actionable. If a merged MR says "closes #42" but #42 is still
|
||||
open, something is wrong. Either auto-close failed (wrong target branch), the
|
||||
reference was incorrect, or the issue needs manual attention.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `entity_references` (reference_type='closes')
|
||||
- `merge_requests` (state='merged')
|
||||
- `issues` (state='opened')
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
mr.iid as mr_iid,
|
||||
mr.title as mr_title,
|
||||
mr.merged_at,
|
||||
mr.target_branch,
|
||||
i.iid as issue_iid,
|
||||
i.title as issue_title,
|
||||
i.state as issue_state,
|
||||
p.path_with_namespace
|
||||
FROM entity_references er
|
||||
JOIN merge_requests mr ON er.source_entity_type = 'merge_request'
|
||||
AND er.source_entity_id = mr.id
|
||||
JOIN issues i ON er.target_entity_type = 'issue'
|
||||
AND er.target_entity_id = i.id
|
||||
JOIN projects p ON er.project_id = p.id
|
||||
WHERE er.reference_type = 'closes'
|
||||
AND mr.state = 'merged'
|
||||
AND i.state = 'opened';
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Closure Gaps — merged MRs that didn't close their referenced issues
|
||||
|
||||
group/backend !234 merged 3d ago → #42 still OPEN
|
||||
"Refactor auth middleware" should have closed "Login timeout bug"
|
||||
Target branch: develop (default: main) — possible branch mismatch
|
||||
|
||||
group/frontend !45 merged 1w ago → #38 still OPEN
|
||||
"Update dashboard" should have closed "Dashboard layout broken"
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Could be intentional (MR merged to wrong branch, issue tracked across branches)
|
||||
- Cross-project references may not be resolvable if target project not synced
|
||||
- GitLab auto-close only works when merging to default branch
|
||||
|
||||
## Extensions
|
||||
|
||||
- Flag likely cause: branch mismatch (target_branch != project.default_branch)
|
||||
- `lore closure-gaps --auto-close` — actually close the issues via API (dangerous, needs confirmation)
|
||||
101
docs/ideas/collaboration.md
Normal file
101
docs/ideas/collaboration.md
Normal file
@@ -0,0 +1,101 @@
|
||||
# Author Collaboration Network
|
||||
|
||||
- **Command:** `lore collaboration [--since <date>]`
|
||||
- **Confidence:** 70%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — self-join on notes, graph construction
|
||||
|
||||
## What
|
||||
|
||||
Build a weighted graph of author pairs: (author_A, author_B, weight) where weight =
|
||||
number of times A reviewed B's MR + B reviewed A's MR + they both commented on the
|
||||
same entity.
|
||||
|
||||
## Why
|
||||
|
||||
Reveals team structure empirically. Shows who collaborates across team boundaries
|
||||
and where knowledge transfer happens. Useful for re-orgs, onboarding planning,
|
||||
and identifying isolated team members.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `merge_requests` (author_username)
|
||||
- `notes` (author_username, note_type='DiffNote')
|
||||
- `discussions` (for co-participation)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
-- Review relationships: who reviews whose MRs
|
||||
SELECT
|
||||
mr.author_username as author,
|
||||
n.author_username as reviewer,
|
||||
COUNT(*) as review_count
|
||||
FROM merge_requests mr
|
||||
JOIN discussions d ON d.merge_request_id = mr.id
|
||||
JOIN notes n ON n.discussion_id = d.id
|
||||
WHERE n.note_type = 'DiffNote'
|
||||
AND n.is_system = 0
|
||||
AND n.author_username != mr.author_username
|
||||
AND mr.created_at >= ?1
|
||||
GROUP BY mr.author_username, n.author_username;
|
||||
|
||||
-- Co-participation: who comments on the same entities
|
||||
WITH entity_participants AS (
|
||||
SELECT
|
||||
COALESCE(d.issue_id, d.merge_request_id) as entity_id,
|
||||
d.noteable_type,
|
||||
n.author_username
|
||||
FROM discussions d
|
||||
JOIN notes n ON n.discussion_id = d.id
|
||||
WHERE n.is_system = 0
|
||||
AND n.created_at >= ?1
|
||||
)
|
||||
SELECT
|
||||
a.author_username as person_a,
|
||||
b.author_username as person_b,
|
||||
COUNT(DISTINCT a.entity_id) as shared_entities
|
||||
FROM entity_participants a
|
||||
JOIN entity_participants b
|
||||
ON a.entity_id = b.entity_id
|
||||
AND a.noteable_type = b.noteable_type
|
||||
AND a.author_username < b.author_username -- avoid duplicates
|
||||
GROUP BY a.author_username, b.author_username;
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
### JSON (for further analysis)
|
||||
```json
|
||||
{
|
||||
"nodes": ["alice", "bob", "charlie"],
|
||||
"edges": [
|
||||
{ "source": "alice", "target": "bob", "reviews": 15, "co_participated": 8 },
|
||||
{ "source": "bob", "target": "charlie", "reviews": 3, "co_participated": 12 }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Human
|
||||
```
|
||||
Collaboration Network (last 90 days)
|
||||
|
||||
alice <-> bob 15 reviews, 8 shared discussions [strong]
|
||||
bob <-> charlie 3 reviews, 12 shared discussions [moderate]
|
||||
alice <-> charlie 1 review, 2 shared discussions [weak]
|
||||
dave <-> (none) 0 reviews, 0 shared discussions [isolated]
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Interpretation requires context; high collaboration might mean dependency
|
||||
- Doesn't capture collaboration outside GitLab
|
||||
- Self-join can be slow with many notes
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore collaboration --format dot` — GraphViz network diagram
|
||||
- `lore collaboration --isolated` — find team members with no collaboration edges
|
||||
- Team boundary detection via graph clustering algorithms
|
||||
86
docs/ideas/contributors.md
Normal file
86
docs/ideas/contributors.md
Normal file
@@ -0,0 +1,86 @@
|
||||
# Contributor Heatmap
|
||||
|
||||
- **Command:** `lore contributors [--since <date>]`
|
||||
- **Confidence:** 88%
|
||||
- **Tier:** 2
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — multiple aggregation queries
|
||||
|
||||
## What
|
||||
|
||||
Rank team members by activity across configurable time windows (7d, 30d, 90d). Shows
|
||||
issues authored, MRs authored, MRs merged, review comments made, discussions
|
||||
participated in.
|
||||
|
||||
## Why
|
||||
|
||||
Team leads constantly ask "who's been active?" or "who's contributing to reviews?"
|
||||
This answers it from local data without GitLab Premium analytics. Also useful for
|
||||
identifying team members who may be overloaded or disengaged.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `issues` (author_username, created_at)
|
||||
- `merge_requests` (author_username, created_at, merged_at)
|
||||
- `notes` (author_username, created_at, note_type, is_system)
|
||||
- `discussions` (for participation counting)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
-- Combined activity per author
|
||||
WITH activity AS (
|
||||
SELECT author_username, 'issue_authored' as activity_type, created_at
|
||||
FROM issues WHERE created_at >= ?1
|
||||
UNION ALL
|
||||
SELECT author_username, 'mr_authored', created_at
|
||||
FROM merge_requests WHERE created_at >= ?1
|
||||
UNION ALL
|
||||
SELECT author_username, 'mr_merged', merged_at
|
||||
FROM merge_requests WHERE merged_at >= ?1 AND state = 'merged'
|
||||
UNION ALL
|
||||
SELECT author_username, 'review_comment', created_at
|
||||
FROM notes WHERE created_at >= ?1 AND note_type = 'DiffNote' AND is_system = 0
|
||||
UNION ALL
|
||||
SELECT author_username, 'discussion_comment', created_at
|
||||
FROM notes WHERE created_at >= ?1 AND note_type != 'DiffNote' AND is_system = 0
|
||||
)
|
||||
SELECT
|
||||
author_username,
|
||||
COUNT(*) FILTER (WHERE activity_type = 'issue_authored') as issues,
|
||||
COUNT(*) FILTER (WHERE activity_type = 'mr_authored') as mrs_authored,
|
||||
COUNT(*) FILTER (WHERE activity_type = 'mr_merged') as mrs_merged,
|
||||
COUNT(*) FILTER (WHERE activity_type = 'review_comment') as reviews,
|
||||
COUNT(*) FILTER (WHERE activity_type = 'discussion_comment') as comments,
|
||||
COUNT(*) as total
|
||||
FROM activity
|
||||
GROUP BY author_username
|
||||
ORDER BY total DESC;
|
||||
```
|
||||
|
||||
Note: SQLite doesn't support FILTER — use SUM(CASE WHEN ... THEN 1 ELSE 0 END).
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Contributors (last 30 days)
|
||||
|
||||
Username Issues MRs Merged Reviews Comments Total
|
||||
alice 3 8 7 23 12 53
|
||||
bob 1 5 4 31 8 49
|
||||
charlie 5 3 2 4 15 29
|
||||
dave 0 1 0 2 3 6
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Could be used for surveillance; frame as team health, not individual tracking
|
||||
- Activity volume != productivity (one thoughtful review > ten "LGTM"s)
|
||||
- Doesn't capture work done outside GitLab
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore contributors --project group/backend` — scoped to project
|
||||
- `lore contributors --type reviews` — focus on review activity only
|
||||
- Trend comparison: `--compare 30d,90d` shows velocity changes
|
||||
94
docs/ideas/decisions.md
Normal file
94
docs/ideas/decisions.md
Normal file
@@ -0,0 +1,94 @@
|
||||
# Decision Archaeology
|
||||
|
||||
- **Command:** `lore decisions <query>`
|
||||
- **Confidence:** 82%
|
||||
- **Tier:** 2
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — search pipeline + regex pattern matching on notes
|
||||
|
||||
## What
|
||||
|
||||
Search for discussion notes that contain decision-making language. Use the existing
|
||||
search pipeline but boost notes containing patterns like "decided", "agreed",
|
||||
"will go with", "tradeoff", "because we", "rationale", "the approach is", "we chose".
|
||||
Return the surrounding discussion context.
|
||||
|
||||
## Why
|
||||
|
||||
This is gitlore's unique value proposition — "why was this decision made?" is the
|
||||
question that no other tool answers well. Architecture Decision Records are rarely
|
||||
maintained; the real decisions live in discussion threads. This mines them.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `documents` + search pipeline (for finding relevant entities)
|
||||
- `notes` (body text for pattern matching)
|
||||
- `discussions` (for thread context)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```
|
||||
1. Run existing hybrid search to find entities matching the query topic
|
||||
2. For each result entity, query all discussion notes
|
||||
3. Score each note against decision-language patterns:
|
||||
- Strong signals (weight 3): "decided to", "agreed on", "the decision is",
|
||||
"we will go with", "approved approach"
|
||||
- Medium signals (weight 2): "tradeoff", "because", "rationale", "chosen",
|
||||
"opted for", "rejected", "alternative"
|
||||
- Weak signals (weight 1): "should we", "proposal", "option A", "option B",
|
||||
"pros and cons"
|
||||
4. Return notes scoring above threshold, with surrounding context (previous and
|
||||
next note in discussion thread)
|
||||
5. Sort by: search relevance * decision score
|
||||
```
|
||||
|
||||
### Decision Patterns (regex)
|
||||
|
||||
```rust
|
||||
const STRONG_PATTERNS: &[&str] = &[
|
||||
r"(?i)\b(decided|agreed|approved)\s+(to|on|that)\b",
|
||||
r"(?i)\bthe\s+(decision|approach|plan)\s+is\b",
|
||||
r"(?i)\bwe('ll| will| are going to)\s+(go with|use|implement)\b",
|
||||
r"(?i)\blet'?s\s+(go with|use|do)\b",
|
||||
];
|
||||
|
||||
const MEDIUM_PATTERNS: &[&str] = &[
|
||||
r"(?i)\b(tradeoff|trade-off|rationale|because we|opted for)\b",
|
||||
r"(?i)\b(rejected|ruled out|won't work|not viable)\b",
|
||||
r"(?i)\b(chosen|selected|picked)\b.{0,20}\b(over|instead of)\b",
|
||||
];
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Decisions related to "authentication"
|
||||
|
||||
group/backend !234 — "Refactor auth middleware"
|
||||
Discussion #a1b2c3 (alice, 3w ago):
|
||||
"We decided to use JWT with short-lived tokens instead of session cookies.
|
||||
The tradeoff is more complexity in the refresh flow, but we get stateless
|
||||
auth which scales better."
|
||||
Decision confidence: HIGH (3 strong pattern matches)
|
||||
|
||||
group/backend #42 — "Auth architecture review"
|
||||
Discussion #d4e5f6 (bob, 2mo ago):
|
||||
"After discussing with the security team, we'll go with bcrypt for password
|
||||
hashing. Argon2 is theoretically better but bcrypt has wider library support."
|
||||
Decision confidence: HIGH (2 strong pattern matches)
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Pattern matching is imperfect; may miss decisions phrased differently
|
||||
- May surface "discussion about deciding" rather than actual decisions
|
||||
- Non-English discussions won't match
|
||||
- Requires good search results as input (garbage in, garbage out)
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore decisions --recent` — decisions made in last 30 days
|
||||
- `lore decisions --author alice` — decisions made by specific person
|
||||
- Export as ADR (Architecture Decision Record) format
|
||||
- Combine with timeline for chronological decision history
|
||||
131
docs/ideas/digest.md
Normal file
131
docs/ideas/digest.md
Normal file
@@ -0,0 +1,131 @@
|
||||
# "What Changed?" Digest
|
||||
|
||||
- **Command:** `lore digest --since <date>`
|
||||
- **Confidence:** 93%
|
||||
- **Tier:** 1
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — multiple queries across event tables, formatting logic
|
||||
|
||||
## What
|
||||
|
||||
Generate a structured summary of all activity since a given date: issues
|
||||
opened/closed, MRs merged, labels changed, milestones updated, key discussions.
|
||||
Group by project and sort by significance (state changes > merges > label changes >
|
||||
new comments).
|
||||
|
||||
Default `--since` is 1 day (last 24 hours). Supports `7d`, `2w`, `YYYY-MM-DD`.
|
||||
|
||||
## Why
|
||||
|
||||
"What happened while I was on PTO?" is the most universal developer question. This
|
||||
is a killer feature that leverages ALL the event data gitlore has ingested. No other
|
||||
local tool provides this.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `resource_state_events` (opened/closed/merged/reopened)
|
||||
- `resource_label_events` (label add/remove)
|
||||
- `resource_milestone_events` (milestone add/remove)
|
||||
- `merge_requests` (merged_at for merge events)
|
||||
- `issues` (created_at for new issues)
|
||||
- `discussions` (last_note_at for active discussions)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```
|
||||
1. Parse --since into ms epoch timestamp
|
||||
2. Query each event table WHERE created_at >= since
|
||||
3. Query new issues WHERE created_at >= since
|
||||
4. Query merged MRs WHERE merged_at >= since
|
||||
5. Query active discussions WHERE last_note_at >= since
|
||||
6. Group all events by project
|
||||
7. Within each project, sort by: state changes first, then merges, then labels
|
||||
8. Format as human-readable sections or robot JSON
|
||||
```
|
||||
|
||||
### SQL Queries
|
||||
|
||||
```sql
|
||||
-- State changes in window
|
||||
SELECT rse.*, i.iid as issue_iid, mr.iid as mr_iid,
|
||||
COALESCE(i.title, mr.title) as title,
|
||||
p.path_with_namespace
|
||||
FROM resource_state_events rse
|
||||
LEFT JOIN issues i ON rse.issue_id = i.id
|
||||
LEFT JOIN merge_requests mr ON rse.merge_request_id = mr.id
|
||||
JOIN projects p ON rse.project_id = p.id
|
||||
WHERE rse.created_at >= ?1
|
||||
ORDER BY rse.created_at DESC;
|
||||
|
||||
-- Newly merged MRs
|
||||
SELECT mr.iid, mr.title, mr.author_username, mr.merged_at,
|
||||
p.path_with_namespace
|
||||
FROM merge_requests mr
|
||||
JOIN projects p ON mr.project_id = p.id
|
||||
WHERE mr.merged_at >= ?1
|
||||
ORDER BY mr.merged_at DESC;
|
||||
|
||||
-- New issues
|
||||
SELECT i.iid, i.title, i.author_username, i.created_at,
|
||||
p.path_with_namespace
|
||||
FROM issues i
|
||||
JOIN projects p ON i.project_id = p.id
|
||||
WHERE i.created_at >= ?1
|
||||
ORDER BY i.created_at DESC;
|
||||
```
|
||||
|
||||
## Human Output Format
|
||||
|
||||
```
|
||||
=== What Changed (last 7 days) ===
|
||||
|
||||
group/backend (12 events)
|
||||
Merged:
|
||||
!234 Refactor auth middleware (alice, 2d ago)
|
||||
!231 Fix connection pool leak (bob, 5d ago)
|
||||
Closed:
|
||||
#89 Login timeout on slow networks (closed by alice, 3d ago)
|
||||
Opened:
|
||||
#95 Rate limiting returns 500 (charlie, 1d ago)
|
||||
Labels:
|
||||
#90 +priority::high (dave, 4d ago)
|
||||
|
||||
group/frontend (3 events)
|
||||
Merged:
|
||||
!45 Update dashboard layout (eve, 6d ago)
|
||||
```
|
||||
|
||||
## Robot Mode Output
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"since": "2025-01-20T00:00:00Z",
|
||||
"projects": [
|
||||
{
|
||||
"path": "group/backend",
|
||||
"merged": [ { "iid": 234, "title": "...", "author": "alice" } ],
|
||||
"closed": [ { "iid": 89, "title": "...", "actor": "alice" } ],
|
||||
"opened": [ { "iid": 95, "title": "...", "author": "charlie" } ],
|
||||
"label_changes": [ { "iid": 90, "label": "priority::high", "action": "add" } ]
|
||||
}
|
||||
],
|
||||
"summary": { "total_events": 15, "projects_active": 2 }
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Can be overwhelming for very active repos; needs `--limit` per category
|
||||
- Doesn't capture nuance (a 200-comment MR merge is more significant than a typo fix)
|
||||
- Only shows what gitlore has synced; stale data = stale digest
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore digest --author alice` — personal activity digest
|
||||
- `lore digest --project group/backend` — single project scope
|
||||
- `lore digest --format markdown` — paste-ready for Slack/email
|
||||
- Combine with weekly-digest for scheduled summaries
|
||||
120
docs/ideas/experts.md
Normal file
120
docs/ideas/experts.md
Normal file
@@ -0,0 +1,120 @@
|
||||
# Who Knows About X?
|
||||
|
||||
- **Command:** `lore experts <path-or-topic>`
|
||||
- **Confidence:** 92%
|
||||
- **Tier:** 1
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — two query paths (file-based, topic-based)
|
||||
|
||||
## What
|
||||
|
||||
Given a file path, find people who have authored MRs touching that file, left
|
||||
DiffNotes on that file, or discussed issues referencing that file. Given a topic
|
||||
string, use search to find relevant entities then extract the active participants.
|
||||
|
||||
## Why
|
||||
|
||||
"Who should I ask about the auth module?" is one of the most common questions in
|
||||
large teams. This answers it empirically from actual contribution and review data.
|
||||
No guessing, no out-of-date wiki pages.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `mr_file_changes` (new_path, merge_request_id) — who changed the file
|
||||
- `notes` (position_new_path, author_username) — who reviewed the file
|
||||
- `merge_requests` (author_username) — MR authorship
|
||||
- `documents` + search pipeline — for topic-based queries
|
||||
- `discussions` + `notes` — for participant extraction
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
### Path Mode: `lore experts src/auth/`
|
||||
|
||||
```
|
||||
1. Query mr_file_changes WHERE new_path LIKE 'src/auth/%'
|
||||
2. Join merge_requests to get author_username for each MR
|
||||
3. Query notes WHERE position_new_path LIKE 'src/auth/%'
|
||||
4. Collect all usernames with activity counts
|
||||
5. Rank by: MR authorship (weight 3) + DiffNote authorship (weight 2) + discussion participation (weight 1)
|
||||
6. Apply recency decay (recent activity weighted higher)
|
||||
```
|
||||
|
||||
### Topic Mode: `lore experts "authentication timeout"`
|
||||
|
||||
```
|
||||
1. Run existing hybrid search for the topic
|
||||
2. Collect top N document results
|
||||
3. For each document, extract author_username
|
||||
4. For each document's entity, query discussions and collect note authors
|
||||
5. Rank by frequency and recency
|
||||
```
|
||||
|
||||
### SQL (Path Mode)
|
||||
|
||||
```sql
|
||||
-- Authors who changed files matching pattern
|
||||
SELECT mr.author_username, COUNT(*) as changes, MAX(mr.merged_at) as last_active
|
||||
FROM mr_file_changes mfc
|
||||
JOIN merge_requests mr ON mfc.merge_request_id = mr.id
|
||||
WHERE mfc.new_path LIKE ?1
|
||||
AND mr.state = 'merged'
|
||||
GROUP BY mr.author_username
|
||||
ORDER BY changes DESC;
|
||||
|
||||
-- Reviewers who commented on files matching pattern
|
||||
SELECT n.author_username, COUNT(*) as reviews, MAX(n.created_at) as last_active
|
||||
FROM notes n
|
||||
WHERE n.position_new_path LIKE ?1
|
||||
AND n.note_type = 'DiffNote'
|
||||
AND n.is_system = 0
|
||||
GROUP BY n.author_username
|
||||
ORDER BY reviews DESC;
|
||||
```
|
||||
|
||||
## Human Output Format
|
||||
|
||||
```
|
||||
Experts for: src/auth/
|
||||
|
||||
alice 12 changes, 8 reviews (last active 3d ago) [top contributor]
|
||||
bob 3 changes, 15 reviews (last active 1d ago) [top reviewer]
|
||||
charlie 5 changes, 2 reviews (last active 2w ago)
|
||||
dave 1 change, 0 reviews (last active 3mo ago) [stale]
|
||||
```
|
||||
|
||||
## Robot Mode Output
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"query": "src/auth/",
|
||||
"query_type": "path",
|
||||
"experts": [
|
||||
{
|
||||
"username": "alice",
|
||||
"changes": 12,
|
||||
"reviews": 8,
|
||||
"discussions": 3,
|
||||
"score": 62,
|
||||
"last_active": "2025-01-25T10:00:00Z",
|
||||
"role": "top_contributor"
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Historical data may be stale (people leave teams, change roles)
|
||||
- Path mode requires `mr_file_changes` to be populated (Gate 4 ingestion)
|
||||
- Topic mode quality depends on search quality
|
||||
- Doesn't account for org chart / actual ownership
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore experts --since 90d` — recency filter
|
||||
- `lore experts --min-activity 3` — noise filter
|
||||
- Combine with `lore silos` to highlight when an expert is the ONLY expert
|
||||
75
docs/ideas/graph.md
Normal file
75
docs/ideas/graph.md
Normal file
@@ -0,0 +1,75 @@
|
||||
# Entity Relationship Explorer
|
||||
|
||||
- **Command:** `lore graph <entity-type> <iid>`
|
||||
- **Confidence:** 80%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — BFS traversal (similar to timeline expand), output formatting
|
||||
|
||||
## What
|
||||
|
||||
Given an issue or MR, traverse `entity_references` and display all connected
|
||||
entities with relationship types and depths. Output as tree, JSON, or Mermaid diagram.
|
||||
|
||||
## Why
|
||||
|
||||
The entity_references graph is already built (Gate 2) but has no dedicated
|
||||
exploration command. Timeline shows events over time; this shows the relationship
|
||||
structure. "What's connected to this issue?" is a different question from "what
|
||||
happened to this issue?"
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `entity_references` (source/target entity, reference_type)
|
||||
- `issues` / `merge_requests` (for entity context)
|
||||
- Timeline expand stage already implements BFS over this graph
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```
|
||||
1. Resolve entity type + iid to local ID
|
||||
2. BFS over entity_references:
|
||||
- Follow source→target AND target→source (bidirectional)
|
||||
- Track depth (--depth flag, default 2)
|
||||
- Track reference_type for edge labels
|
||||
3. Hydrate each discovered entity with title, state, URL
|
||||
4. Format as tree / JSON / Mermaid
|
||||
```
|
||||
|
||||
## Human Output (Tree)
|
||||
|
||||
```
|
||||
#42 Login timeout bug (CLOSED)
|
||||
├── closes ── !234 Refactor auth middleware (MERGED)
|
||||
│ ├── mentioned ── #38 Connection timeout in auth flow (CLOSED)
|
||||
│ └── mentioned ── #51 Token refresh improvements (OPEN)
|
||||
├── related ── #45 Auth module documentation (OPEN)
|
||||
└── mentioned ── !228 Database migration (MERGED)
|
||||
└── closes ── #35 Schema version drift (CLOSED)
|
||||
```
|
||||
|
||||
## Mermaid Output
|
||||
|
||||
```mermaid
|
||||
graph LR
|
||||
I42["#42 Login timeout"] -->|closes| MR234["!234 Refactor auth"]
|
||||
MR234 -->|mentioned| I38["#38 Connection timeout"]
|
||||
MR234 -->|mentioned| I51["#51 Token refresh"]
|
||||
I42 -->|related| I45["#45 Auth docs"]
|
||||
I42 -->|mentioned| MR228["!228 DB migration"]
|
||||
MR228 -->|closes| I35["#35 Schema drift"]
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Overlaps somewhat with timeline (but different focus: structure vs chronology)
|
||||
- High fan-out for popular entities (need depth + limit controls)
|
||||
- Unresolved cross-project references appear as dead ends
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore graph --format dot` — GraphViz DOT output
|
||||
- `lore graph --format mermaid` — Mermaid diagram
|
||||
- `lore graph --include-discussions` — show discussion threads as nodes
|
||||
- Interactive HTML visualization (future web UI)
|
||||
70
docs/ideas/hotspots.md
Normal file
70
docs/ideas/hotspots.md
Normal file
@@ -0,0 +1,70 @@
|
||||
# File Hotspot Report
|
||||
|
||||
- **Command:** `lore hotspots [--since <date>]`
|
||||
- **Confidence:** 85%
|
||||
- **Tier:** 2
|
||||
- **Status:** proposed
|
||||
- **Effort:** low — single query on mr_file_changes (requires Gate 4 population)
|
||||
|
||||
## What
|
||||
|
||||
Rank files by frequency of appearance in merged MRs over a time window. Show
|
||||
change_type breakdown (modified vs added vs deleted). Optionally filter by project.
|
||||
|
||||
## Why
|
||||
|
||||
Hot files are where bugs live. This is a proven engineering metric (see "Your Code
|
||||
as a Crime Scene" by Adam Tornhill). High-churn files deserve extra test coverage,
|
||||
better documentation, and architectural review.
|
||||
|
||||
## Data Required
|
||||
|
||||
- `mr_file_changes` (new_path, change_type, merge_request_id) — needs Gate 4 population
|
||||
- `merge_requests` (merged_at, state='merged')
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
mfc.new_path,
|
||||
p.path_with_namespace,
|
||||
COUNT(*) as total_changes,
|
||||
SUM(CASE WHEN mfc.change_type = 'modified' THEN 1 ELSE 0 END) as modifications,
|
||||
SUM(CASE WHEN mfc.change_type = 'added' THEN 1 ELSE 0 END) as additions,
|
||||
SUM(CASE WHEN mfc.change_type = 'deleted' THEN 1 ELSE 0 END) as deletions,
|
||||
SUM(CASE WHEN mfc.change_type = 'renamed' THEN 1 ELSE 0 END) as renames,
|
||||
COUNT(DISTINCT mr.author_username) as unique_authors
|
||||
FROM mr_file_changes mfc
|
||||
JOIN merge_requests mr ON mfc.merge_request_id = mr.id
|
||||
JOIN projects p ON mfc.project_id = p.id
|
||||
WHERE mr.state = 'merged'
|
||||
AND mr.merged_at >= ?1
|
||||
GROUP BY mfc.new_path, p.path_with_namespace
|
||||
ORDER BY total_changes DESC
|
||||
LIMIT ?2;
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
File Hotspots (last 90 days, top 20)
|
||||
|
||||
File Changes Authors Type Breakdown
|
||||
src/auth/middleware.rs 18 4 14 mod, 3 add, 1 del
|
||||
src/api/routes.rs 15 3 12 mod, 2 add, 1 rename
|
||||
src/db/migrations.rs 12 2 8 mod, 4 add
|
||||
tests/integration/auth_test.rs 11 3 9 mod, 2 add
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Requires `mr_file_changes` to be populated (Gate 4 ingestion)
|
||||
- Doesn't distinguish meaningful changes from trivial ones (formatting, imports)
|
||||
- Configuration files (CI, Cargo.toml) will rank high but aren't risky
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore hotspots --exclude "*.toml,*.yml"` — filter out config files
|
||||
- `lore hotspots --dir src/auth/` — scope to directory
|
||||
- Combine with `lore silos` for risk scoring: high churn + bus factor 1 = critical
|
||||
- Complexity trend: correlate with discussion count (churn + many discussions = problematic)
|
||||
69
docs/ideas/idle.md
Normal file
69
docs/ideas/idle.md
Normal file
@@ -0,0 +1,69 @@
|
||||
# Idle Work Detector
|
||||
|
||||
- **Command:** `lore idle [--days <N>] [--labels <pattern>]`
|
||||
- **Confidence:** 73%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — label event querying with configurable patterns
|
||||
|
||||
## What
|
||||
|
||||
Find entities that received an "in progress" or similar label but have had no
|
||||
discussion activity for N days. Cross-reference with assignee to show who might
|
||||
have forgotten about something.
|
||||
|
||||
## Why
|
||||
|
||||
Forgotten WIP is invisible waste. Developers start work, get pulled to something
|
||||
urgent, and the original task sits idle. This makes it visible before it becomes
|
||||
a problem.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `resource_label_events` (label_name, action='add', created_at)
|
||||
- `discussions` (last_note_at for entity activity)
|
||||
- `issues` / `merge_requests` (state, assignees)
|
||||
- `issue_assignees` / `mr_assignees`
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```
|
||||
1. Query resource_label_events for labels matching "in progress" patterns
|
||||
Default patterns: "in-progress", "in_progress", "doing", "wip",
|
||||
"workflow::in-progress", "status::in-progress"
|
||||
Configurable via --labels flag
|
||||
2. For each entity with an "in progress" label still applied:
|
||||
a. Check if the label was subsequently removed (if so, skip)
|
||||
b. Get last_note_at from discussions for that entity
|
||||
c. Flag if last_note_at is older than threshold
|
||||
3. Join with assignees for attribution
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Idle Work (labeled "in progress" but no activity for 14+ days)
|
||||
|
||||
group/backend
|
||||
#90 Rate limiting design assigned to: charlie idle 18 days
|
||||
Last activity: label +priority::high by dave
|
||||
#85 Cache invalidation fix assigned to: alice idle 21 days
|
||||
Last activity: discussion comment by bob
|
||||
|
||||
group/frontend
|
||||
!230 Dashboard redesign assigned to: eve idle 14 days
|
||||
Last activity: DiffNote by dave
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Requires label naming conventions; no universal standard
|
||||
- Work may be happening outside GitLab (local branch, design doc)
|
||||
- "Idle" threshold is subjective; 14 days may be normal for large features
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore idle --assignee alice` — personal idle work check
|
||||
- `lore idle --notify` — generate message templates for nudging owners
|
||||
- Configurable label patterns in config.json for team-specific workflows
|
||||
92
docs/ideas/impact-graph.md
Normal file
92
docs/ideas/impact-graph.md
Normal file
@@ -0,0 +1,92 @@
|
||||
# Cross-Project Impact Graph
|
||||
|
||||
- **Command:** `lore impact-graph [--format json|dot|mermaid]`
|
||||
- **Confidence:** 75%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — aggregation over entity_references, graph output formatting
|
||||
|
||||
## What
|
||||
|
||||
Aggregate `entity_references` by project pair to produce a weighted adjacency matrix
|
||||
showing how projects reference each other. Output as JSON, DOT, or Mermaid for
|
||||
visualization.
|
||||
|
||||
## Why
|
||||
|
||||
Makes invisible architectural coupling visible. "Backend and frontend repos have
|
||||
47 cross-references this quarter" tells you about tight coupling that may need
|
||||
architectural attention.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `entity_references` (source/target entity IDs)
|
||||
- `issues` / `merge_requests` (project_id for source/target)
|
||||
- `projects` (path_with_namespace)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
-- Project-to-project reference counts
|
||||
WITH ref_projects AS (
|
||||
SELECT
|
||||
CASE er.source_entity_type
|
||||
WHEN 'issue' THEN i_src.project_id
|
||||
WHEN 'merge_request' THEN mr_src.project_id
|
||||
END as source_project_id,
|
||||
CASE er.target_entity_type
|
||||
WHEN 'issue' THEN i_tgt.project_id
|
||||
WHEN 'merge_request' THEN mr_tgt.project_id
|
||||
END as target_project_id,
|
||||
er.reference_type
|
||||
FROM entity_references er
|
||||
LEFT JOIN issues i_src ON er.source_entity_type = 'issue' AND er.source_entity_id = i_src.id
|
||||
LEFT JOIN merge_requests mr_src ON er.source_entity_type = 'merge_request' AND er.source_entity_id = mr_src.id
|
||||
LEFT JOIN issues i_tgt ON er.target_entity_type = 'issue' AND er.target_entity_id = i_tgt.id
|
||||
LEFT JOIN merge_requests mr_tgt ON er.target_entity_type = 'merge_request' AND er.target_entity_id = mr_tgt.id
|
||||
WHERE er.target_entity_id IS NOT NULL -- resolved references only
|
||||
)
|
||||
SELECT
|
||||
p_src.path_with_namespace as source_project,
|
||||
p_tgt.path_with_namespace as target_project,
|
||||
er.reference_type,
|
||||
COUNT(*) as weight
|
||||
FROM ref_projects rp
|
||||
JOIN projects p_src ON rp.source_project_id = p_src.id
|
||||
JOIN projects p_tgt ON rp.target_project_id = p_tgt.id
|
||||
WHERE rp.source_project_id != rp.target_project_id -- cross-project only
|
||||
GROUP BY p_src.path_with_namespace, p_tgt.path_with_namespace, er.reference_type
|
||||
ORDER BY weight DESC;
|
||||
```
|
||||
|
||||
## Output Formats
|
||||
|
||||
### Mermaid
|
||||
```mermaid
|
||||
graph LR
|
||||
Backend -->|closes 23| Frontend
|
||||
Backend -->|mentioned 47| Infrastructure
|
||||
Frontend -->|mentioned 12| Backend
|
||||
```
|
||||
|
||||
### DOT
|
||||
```dot
|
||||
digraph impact {
|
||||
"group/backend" -> "group/frontend" [label="closes: 23"];
|
||||
"group/backend" -> "group/infra" [label="mentioned: 47"];
|
||||
}
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Requires multiple projects synced; limited value for single-project users
|
||||
- "Mentioned" references are noisy (high volume, low signal)
|
||||
- Doesn't capture coupling through shared libraries or APIs (code-level coupling)
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore impact-graph --since 90d` — time-scoped coupling analysis
|
||||
- `lore impact-graph --type closes` — only meaningful reference types
|
||||
- Include unresolved references to show dependencies on un-synced projects
|
||||
- Coupling trend: is cross-project coupling increasing over time?
|
||||
97
docs/ideas/label-audit.md
Normal file
97
docs/ideas/label-audit.md
Normal file
@@ -0,0 +1,97 @@
|
||||
# Label Hygiene Audit
|
||||
|
||||
- **Command:** `lore label-audit`
|
||||
- **Confidence:** 82%
|
||||
- **Tier:** 2
|
||||
- **Status:** proposed
|
||||
- **Effort:** low — straightforward aggregation queries
|
||||
|
||||
## What
|
||||
|
||||
Report on label health:
|
||||
- Labels used only once (may be typos or abandoned experiments)
|
||||
- Labels applied and removed within 1 hour (likely mistakes)
|
||||
- Labels with no active issues/MRs (orphaned)
|
||||
- Label name collisions across projects (same name, different meaning)
|
||||
- Labels never used at all (defined but not applied)
|
||||
|
||||
## Why
|
||||
|
||||
Label sprawl is real and makes filtering useless over time. Teams create labels
|
||||
ad-hoc and never clean them up. This simple audit surfaces maintenance tasks.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `labels` (name, project_id)
|
||||
- `issue_labels` / `mr_labels` (usage counts)
|
||||
- `resource_label_events` (add/remove pairs for mistake detection)
|
||||
- `issues` / `merge_requests` (state for "active" filtering)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
-- Labels used only once
|
||||
SELECT l.name, p.path_with_namespace, COUNT(*) as usage
|
||||
FROM labels l
|
||||
JOIN projects p ON l.project_id = p.id
|
||||
LEFT JOIN issue_labels il ON il.label_id = l.id
|
||||
LEFT JOIN mr_labels ml ON ml.label_id = l.id
|
||||
GROUP BY l.id
|
||||
HAVING COUNT(il.issue_id) + COUNT(ml.merge_request_id) = 1;
|
||||
|
||||
-- Flash labels (applied and removed within 1 hour)
|
||||
SELECT
|
||||
rle1.label_name,
|
||||
rle1.created_at as added_at,
|
||||
rle2.created_at as removed_at,
|
||||
(rle2.created_at - rle1.created_at) / 60000 as minutes_active
|
||||
FROM resource_label_events rle1
|
||||
JOIN resource_label_events rle2
|
||||
ON rle1.issue_id = rle2.issue_id
|
||||
AND rle1.label_name = rle2.label_name
|
||||
AND rle1.action = 'add'
|
||||
AND rle2.action = 'remove'
|
||||
AND rle2.created_at > rle1.created_at
|
||||
AND (rle2.created_at - rle1.created_at) < 3600000;
|
||||
|
||||
-- Unused labels (defined but never applied)
|
||||
SELECT l.name, p.path_with_namespace
|
||||
FROM labels l
|
||||
JOIN projects p ON l.project_id = p.id
|
||||
LEFT JOIN issue_labels il ON il.label_id = l.id
|
||||
LEFT JOIN mr_labels ml ON ml.label_id = l.id
|
||||
WHERE il.issue_id IS NULL AND ml.merge_request_id IS NULL;
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Label Audit
|
||||
|
||||
Unused Labels (4):
|
||||
group/backend: deprecated-v1, needs-triage, wontfix-maybe
|
||||
group/frontend: old-design
|
||||
|
||||
Single-Use Labels (3):
|
||||
group/backend: perf-regression (1 issue)
|
||||
group/frontend: ux-debt (1 MR), mobile-only (1 issue)
|
||||
|
||||
Flash Labels (applied < 1hr, 2):
|
||||
group/backend #90: +priority::critical then -priority::critical (12 min)
|
||||
group/backend #85: +blocked then -blocked (5 min)
|
||||
|
||||
Cross-Project Collisions (1):
|
||||
"needs-review" used in group/backend (32 uses) AND group/frontend (8 uses)
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Low glamour; this is janitorial work
|
||||
- Single-use labels may be legitimate (one-off categorization)
|
||||
- Cross-project collisions may be intentional (shared vocabulary)
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore label-audit --fix` — suggest deletions for unused labels
|
||||
- Trend: label count over time (is sprawl increasing?)
|
||||
74
docs/ideas/label-flow.md
Normal file
74
docs/ideas/label-flow.md
Normal file
@@ -0,0 +1,74 @@
|
||||
# Label Velocity
|
||||
|
||||
- **Command:** `lore label-flow <from-label> <to-label>`
|
||||
- **Confidence:** 78%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — self-join on resource_label_events, percentile computation
|
||||
|
||||
## What
|
||||
|
||||
For a given label pair (e.g., "needs-review" to "approved"), compute median and P90
|
||||
transition times using `resource_label_events`. Shows how fast work moves through
|
||||
your process labels.
|
||||
|
||||
Also supports: single label dwell time (how long does "in-progress" stay applied?).
|
||||
|
||||
## Why
|
||||
|
||||
Process bottlenecks become quantifiable. "Our code review takes a median of 3 days"
|
||||
is actionable data for retrospectives and process improvement.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `resource_label_events` (label_name, action, created_at, issue_id, merge_request_id)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
-- Label A → Label B transition time
|
||||
WITH add_a AS (
|
||||
SELECT issue_id, merge_request_id, MIN(created_at) as added_at
|
||||
FROM resource_label_events
|
||||
WHERE label_name = ?1 AND action = 'add'
|
||||
GROUP BY issue_id, merge_request_id
|
||||
),
|
||||
add_b AS (
|
||||
SELECT issue_id, merge_request_id, MIN(created_at) as added_at
|
||||
FROM resource_label_events
|
||||
WHERE label_name = ?2 AND action = 'add'
|
||||
GROUP BY issue_id, merge_request_id
|
||||
)
|
||||
SELECT
|
||||
(b.added_at - a.added_at) / 3600000.0 as hours_transition
|
||||
FROM add_a a
|
||||
JOIN add_b b ON a.issue_id = b.issue_id OR a.merge_request_id = b.merge_request_id
|
||||
WHERE b.added_at > a.added_at;
|
||||
```
|
||||
|
||||
Then compute percentiles in Rust (median, P75, P90).
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Label Flow: "needs-review" → "approved"
|
||||
|
||||
Transitions: 42 issues/MRs in last 90 days
|
||||
Median: 18.5 hours
|
||||
P75: 36.2 hours
|
||||
P90: 72.8 hours
|
||||
Slowest: !234 Refactor auth (168 hours)
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Only works if teams use label-based workflows consistently
|
||||
- Labels may be applied out of order or skipped
|
||||
- Self-join performance could be slow with many events
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore label-flow --dwell "in-progress"` — how long does a label stay?
|
||||
- `lore label-flow --all` — auto-discover common transitions from event data
|
||||
- Visualization: label state machine with median transition times on edges
|
||||
81
docs/ideas/milestone-risk.md
Normal file
81
docs/ideas/milestone-risk.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Milestone Risk Report
|
||||
|
||||
- **Command:** `lore milestone-risk [title]`
|
||||
- **Confidence:** 78%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — milestone + issue aggregation with scope change detection
|
||||
|
||||
## What
|
||||
|
||||
For each active milestone (or a specific one): show total issues, % closed, issues
|
||||
added after milestone creation (scope creep), issues with no assignee, issues with
|
||||
overdue due_date. Flag milestones where completion rate is below expected trajectory.
|
||||
|
||||
## Why
|
||||
|
||||
Milestone health is usually assessed by gut feel. This provides objective signals
|
||||
from data already ingested. Project managers can spot risks early.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `milestones` (title, state, due_date)
|
||||
- `issues` (milestone_id, state, created_at, due_date, assignee)
|
||||
- `issue_assignees` (for unassigned detection)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
m.title,
|
||||
m.state,
|
||||
m.due_date,
|
||||
COUNT(*) as total_issues,
|
||||
SUM(CASE WHEN i.state = 'closed' THEN 1 ELSE 0 END) as closed,
|
||||
SUM(CASE WHEN i.state = 'opened' THEN 1 ELSE 0 END) as open,
|
||||
SUM(CASE WHEN i.created_at > m.created_at THEN 1 ELSE 0 END) as scope_creep,
|
||||
SUM(CASE WHEN ia.username IS NULL AND i.state = 'opened' THEN 1 ELSE 0 END) as unassigned,
|
||||
SUM(CASE WHEN i.due_date < DATE('now') AND i.state = 'opened' THEN 1 ELSE 0 END) as overdue
|
||||
FROM milestones m
|
||||
JOIN issues i ON i.milestone_id = m.id
|
||||
LEFT JOIN issue_assignees ia ON ia.issue_id = i.id
|
||||
WHERE m.state = 'active'
|
||||
GROUP BY m.id;
|
||||
```
|
||||
|
||||
Note: `created_at` comparison for scope creep is approximate — GitLab doesn't
|
||||
expose when an issue was added to a milestone via its milestone_events.
|
||||
|
||||
Actually we DO have `resource_milestone_events` — use those for precise scope change
|
||||
detection.
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Milestone Risk Report
|
||||
|
||||
v2.0 (due Feb 15, 2025)
|
||||
Progress: 14/20 closed (70%)
|
||||
Scope: +3 issues added after milestone start
|
||||
Risks: 2 issues overdue, 1 issue unassigned
|
||||
Status: ON TRACK (70% complete, 60% time elapsed)
|
||||
|
||||
v2.1 (due Mar 30, 2025)
|
||||
Progress: 2/15 closed (13%)
|
||||
Scope: +8 issues added after milestone start
|
||||
Risks: 5 issues unassigned
|
||||
Status: AT RISK (13% complete, scope still growing)
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Milestone semantics vary wildly between teams
|
||||
- "Scope creep" detection is noisy if teams batch-add issues to milestones
|
||||
- due_date comparison assumes consistent timezone handling
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore milestone-risk --history` — show scope changes over time
|
||||
- Velocity estimation: at current closure rate, will the milestone finish on time?
|
||||
- Combine with label-flow for "how fast are milestone issues moving through workflow"
|
||||
67
docs/ideas/mr-pipeline.md
Normal file
67
docs/ideas/mr-pipeline.md
Normal file
@@ -0,0 +1,67 @@
|
||||
# MR Pipeline Efficiency
|
||||
|
||||
- **Command:** `lore mr-pipeline [--since <date>]`
|
||||
- **Confidence:** 78%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — builds on bottleneck detector with more stages
|
||||
|
||||
## What
|
||||
|
||||
Track the full MR lifecycle: creation, first review, all reviews complete (threads
|
||||
resolved), approval, merge. Compute time spent in each stage across all MRs.
|
||||
Identify which stage is the bottleneck.
|
||||
|
||||
## Why
|
||||
|
||||
"Our merge process is slow" is vague. This breaks it into stages so teams can target
|
||||
the actual bottleneck. Maybe creation-to-review is fast but review-to-merge is slow
|
||||
(merge queue issues). Maybe first review is fast but resolution takes forever
|
||||
(contentious code).
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `merge_requests` (created_at, merged_at)
|
||||
- `notes` (note_type='DiffNote', created_at, author_username)
|
||||
- `discussions` (resolved, resolvable, merge_request_id)
|
||||
- `resource_state_events` (state changes with timestamps)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
For each merged MR, compute:
|
||||
1. **Created → First Review**: MIN(DiffNote.created_at) - mr.created_at
|
||||
2. **First Review → All Resolved**: MAX(discussion.resolved_at) - MIN(DiffNote.created_at)
|
||||
3. **All Resolved → Merged**: mr.merged_at - MAX(discussion.resolved_at)
|
||||
|
||||
Note: "resolved_at" isn't directly stored but can be approximated from the last
|
||||
note in resolved discussions, or from state events.
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
MR Pipeline (last 30 days, 24 merged MRs)
|
||||
|
||||
Stage Median P75 P90
|
||||
Created → First Review 4.2h 12.1h 28.3h
|
||||
First Review → Resolved 8.1h 24.5h 72.0h <-- BOTTLENECK
|
||||
Resolved → Merged 0.5h 1.2h 3.1h
|
||||
|
||||
Total (Created → Merged) 18.4h 48.2h 96.1h
|
||||
|
||||
Biggest bottleneck: Review resolution (median 8.1h)
|
||||
Suggestion: Consider breaking large MRs into smaller reviewable chunks
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- "Resolved" timestamp approximation may be inaccurate
|
||||
- Pipeline assumes linear flow; real MRs have back-and-forth cycles
|
||||
- Draft MRs skew metrics (created early, reviewed late intentionally)
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore mr-pipeline --exclude-drafts` — cleaner metrics
|
||||
- Per-project comparison: which project has the fastest pipeline?
|
||||
- Trend line: weekly pipeline speed over time
|
||||
- Break down by MR size (files changed) to normalize
|
||||
265
docs/ideas/project-ergonomics.md
Normal file
265
docs/ideas/project-ergonomics.md
Normal file
@@ -0,0 +1,265 @@
|
||||
# Multi-Project Ergonomics
|
||||
|
||||
- **Confidence:** 90%
|
||||
- **Tier:** 1
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium (multiple small improvements that compound)
|
||||
|
||||
## The Problem
|
||||
|
||||
Every command that touches project-scoped data requires `-p group/subgroup/project`
|
||||
to disambiguate. For users with 5+ projects synced, this is:
|
||||
|
||||
- Repetitive: typing `-p infra/platform/auth-service` on every query
|
||||
- Error-prone: mistyping long paths
|
||||
- Discoverable only by failure: you don't know you need `-p` until you hit an
|
||||
ambiguous error
|
||||
|
||||
The fuzzy matching in `resolve_project` is already good (suffix, substring,
|
||||
case-insensitive) but it only kicks in on the `-p` value itself. There's no way to
|
||||
set a default, group projects, or scope a whole session.
|
||||
|
||||
## Proposed Improvements
|
||||
|
||||
### 1. Project Aliases in Config
|
||||
|
||||
Let users define short aliases for long project paths.
|
||||
|
||||
```json
|
||||
{
|
||||
"projects": [
|
||||
{ "path": "infra/platform/auth-service", "alias": "auth" },
|
||||
{ "path": "infra/platform/billing-service", "alias": "billing" },
|
||||
{ "path": "frontend/customer-portal", "alias": "portal" },
|
||||
{ "path": "frontend/admin-dashboard", "alias": "admin" }
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
Then: `lore issues -p auth` resolves via alias before falling through to fuzzy match.
|
||||
|
||||
**Implementation:** Add optional `alias` field to `ProjectConfig`. In
|
||||
`resolve_project`, check aliases before the existing exact/suffix/substring cascade.
|
||||
|
||||
```rust
|
||||
#[derive(Debug, Clone, Deserialize)]
|
||||
pub struct ProjectConfig {
|
||||
pub path: String,
|
||||
#[serde(default)]
|
||||
pub alias: Option<String>,
|
||||
}
|
||||
```
|
||||
|
||||
Resolution order becomes:
|
||||
1. Exact alias match (new)
|
||||
2. Exact path match
|
||||
3. Case-insensitive path match
|
||||
4. Suffix match
|
||||
5. Substring match
|
||||
|
||||
### 2. Default Project (`LORE_PROJECT` env var)
|
||||
|
||||
Set a default project for your shell session so you don't need `-p` at all.
|
||||
|
||||
```bash
|
||||
export LORE_PROJECT=auth
|
||||
lore issues # scoped to auth-service
|
||||
lore mrs --state opened # scoped to auth-service
|
||||
lore search "timeout bug" # scoped to auth-service
|
||||
lore issues -p billing # explicit -p overrides the env var
|
||||
```
|
||||
|
||||
**Implementation:** In every command that accepts `-p`, fall back to
|
||||
`std::env::var("LORE_PROJECT")` when the flag is absent. The `-p` flag always wins.
|
||||
|
||||
Could also support a config-level default:
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultProject": "auth"
|
||||
}
|
||||
```
|
||||
|
||||
Precedence: CLI flag > env var > config default > (no filter).
|
||||
|
||||
### 3. `lore use <project>` — Session Context Switcher
|
||||
|
||||
A command that sets `LORE_PROJECT` for the current shell by writing to a dotfile.
|
||||
|
||||
```bash
|
||||
lore use auth
|
||||
# writes ~/.local/state/lore/current-project containing "auth"
|
||||
|
||||
lore issues # reads current-project file, scopes to auth
|
||||
lore use --clear # removes the file, back to all-project mode
|
||||
lore use # shows current project context
|
||||
```
|
||||
|
||||
This is similar to `kubectl config use-context`, `nvm use`, or `tfenv use`.
|
||||
|
||||
**Implementation:** Write a one-line file at a known state path. Each command reads
|
||||
it as the lowest-priority default (below env var and CLI flag).
|
||||
|
||||
Precedence: CLI flag > env var > `lore use` state file > config default > (no filter).
|
||||
|
||||
### 4. `lore projects` — Project Listing and Discovery
|
||||
|
||||
A dedicated command to see what's synced, with aliases and activity stats.
|
||||
|
||||
```bash
|
||||
$ lore projects
|
||||
|
||||
Alias Path Issues MRs Last Sync
|
||||
auth infra/platform/auth-service 142 87 2h ago
|
||||
billing infra/platform/billing-service 56 34 2h ago
|
||||
portal frontend/customer-portal 203 112 2h ago
|
||||
admin frontend/admin-dashboard 28 15 3d ago
|
||||
- data/ml-pipeline 89 45 2h ago
|
||||
```
|
||||
|
||||
Robot mode returns the same as JSON with alias, path, counts, and last sync time.
|
||||
|
||||
**Implementation:** Query `projects` joined with `COUNT(issues)`, `COUNT(mrs)`,
|
||||
and `MAX(sync_runs.finished_at)`. Overlay aliases from config.
|
||||
|
||||
### 5. Project Groups in Config
|
||||
|
||||
Let users define named groups of projects for batch scoping.
|
||||
|
||||
```json
|
||||
{
|
||||
"projectGroups": {
|
||||
"backend": ["auth", "billing", "data/ml-pipeline"],
|
||||
"frontend": ["portal", "admin"],
|
||||
"all-infra": ["auth", "billing"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Then: `lore issues -p @backend` (or `--group backend`) queries across all projects
|
||||
in the group.
|
||||
|
||||
**Implementation:** When `-p` value starts with `@`, look up the group and resolve
|
||||
each member project. Pass as a `Vec<i64>` of project IDs to the query layer.
|
||||
|
||||
This is especially powerful for:
|
||||
- `lore search "auth bug" -p @backend` — search across related repos
|
||||
- `lore digest --since 7d -p @frontend` — team-scoped activity digest
|
||||
- `lore timeline "deployment" -p @all-infra` — cross-repo timeline
|
||||
|
||||
### 6. Git-Aware Project Detection
|
||||
|
||||
When running `lore` from inside a git repo that matches a synced project, auto-scope
|
||||
to that project without any flags.
|
||||
|
||||
```bash
|
||||
cd ~/code/auth-service
|
||||
lore issues # auto-detects this is infra/platform/auth-service
|
||||
```
|
||||
|
||||
**Implementation:** Read `.git/config` for the remote URL, extract the project path,
|
||||
check if it matches a synced project. Only activate when exactly one project matches.
|
||||
|
||||
Detection logic:
|
||||
```
|
||||
1. Check if cwd is inside a git repo (find .git)
|
||||
2. Parse git remote origin URL
|
||||
3. Extract path component (e.g., "infra/platform/auth-service.git" → "infra/platform/auth-service")
|
||||
4. Match against synced projects
|
||||
5. If exactly one match, use as implicit -p
|
||||
6. If ambiguous or no match, do nothing (fall through to normal behavior)
|
||||
```
|
||||
|
||||
Precedence: CLI flag > env var > `lore use` > config default > git detection > (no filter).
|
||||
|
||||
This is similar to how `gh` (GitHub CLI) auto-detects the repo you're in.
|
||||
|
||||
### 7. Prompt Integration / Shell Function
|
||||
|
||||
Provide a shell function that shows the current project context in the prompt.
|
||||
|
||||
```bash
|
||||
# In .bashrc / .zshrc
|
||||
eval "$(lore completions zsh)"
|
||||
PROMPT='$(lore-prompt)%~ %# '
|
||||
```
|
||||
|
||||
Output: `[lore:auth] ~/code/auth-service %`
|
||||
|
||||
Shows which project `lore` commands will scope to, using the same precedence chain.
|
||||
Helps users understand what context they're in before running a query.
|
||||
|
||||
### 8. Short Project References in Output
|
||||
|
||||
Once aliases exist, use them everywhere in output for brevity:
|
||||
|
||||
**Before:**
|
||||
```
|
||||
infra/platform/auth-service#42 Login timeout bug
|
||||
infra/platform/auth-service!234 Refactor auth middleware
|
||||
```
|
||||
|
||||
**After:**
|
||||
```
|
||||
auth#42 Login timeout bug
|
||||
auth!234 Refactor auth middleware
|
||||
```
|
||||
|
||||
With `--full-paths` flag to get the verbose form when needed.
|
||||
|
||||
## Combined UX Flow
|
||||
|
||||
With all improvements, a typical session looks like:
|
||||
|
||||
```bash
|
||||
# One-time config
|
||||
lore init # sets up aliases during interactive setup
|
||||
|
||||
# Daily use
|
||||
lore use auth # set context
|
||||
lore issues --state opened # no -p needed
|
||||
lore search "timeout" # scoped to auth
|
||||
lore timeline "login flow" # scoped to auth
|
||||
lore issues -p @backend # cross-repo query via group
|
||||
lore mrs -p billing # quick alias switch
|
||||
lore use --clear # back to global
|
||||
```
|
||||
|
||||
Or for the power user who never wants to type `lore use`:
|
||||
|
||||
```bash
|
||||
cd ~/code/auth-service
|
||||
lore issues # git-aware auto-detection
|
||||
```
|
||||
|
||||
Or for the scripter:
|
||||
|
||||
```bash
|
||||
LORE_PROJECT=auth lore --robot issues -n 50 # env var for automation
|
||||
```
|
||||
|
||||
## Priority Order
|
||||
|
||||
Implement in this order for maximum incremental value:
|
||||
|
||||
1. **Project aliases** — smallest change, biggest daily friction reduction
|
||||
2. **`LORE_PROJECT` env var** — trivial to implement, enables scripting
|
||||
3. **`lore projects` command** — discoverability, completes the alias story
|
||||
4. **`lore use` context** — nice-to-have for heavy users
|
||||
5. **Project groups** — high value for multi-repo teams
|
||||
6. **Git-aware detection** — polish, "it just works" feel
|
||||
7. **Short refs in output** — ties into timeline issue #001
|
||||
8. **Prompt integration** — extra polish
|
||||
|
||||
## Relationship to Issue #001
|
||||
|
||||
The timeline entity-ref ambiguity (issue #001) is solved naturally by items 7 and 8
|
||||
here. Once aliases exist, `format_entity_ref` can use the alias as the short project
|
||||
identifier in multi-project output:
|
||||
|
||||
```
|
||||
auth#42 instead of infra/platform/auth-service#42
|
||||
```
|
||||
|
||||
And in single-project timelines (detected via `lore use` or git-aware), the project
|
||||
prefix is omitted entirely — matching the current behavior but now intentionally.
|
||||
81
docs/ideas/recurring-patterns.md
Normal file
81
docs/ideas/recurring-patterns.md
Normal file
@@ -0,0 +1,81 @@
|
||||
# Recurring Bug Pattern Detector
|
||||
|
||||
- **Command:** `lore recurring-patterns [--min-cluster <N>]`
|
||||
- **Confidence:** 76%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** high — vector clustering, threshold tuning
|
||||
|
||||
## What
|
||||
|
||||
Cluster closed issues by embedding similarity. Identify clusters of 3+ issues that
|
||||
are semantically similar — these represent recurring problems that need a systemic
|
||||
fix rather than one-off patches.
|
||||
|
||||
## Why
|
||||
|
||||
Finding the same bug filed 5 different ways is one of the most impactful things you
|
||||
can surface. This is a sophisticated use of the embedding pipeline that no competing
|
||||
tool offers. It turns "we keep having auth issues" from a gut feeling into data.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `documents` (source_type='issue', content_text)
|
||||
- `embeddings` (768-dim vectors)
|
||||
- `issues` (state='closed' for filtering)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```
|
||||
1. Collect all embeddings for closed issue documents
|
||||
2. For each issue, find K nearest neighbors (K=10)
|
||||
3. Build adjacency graph: edge exists if similarity > threshold (e.g., 0.80)
|
||||
4. Find connected components (simple DFS/BFS)
|
||||
5. Filter to components with >= min-cluster members (default 3)
|
||||
6. For each cluster:
|
||||
a. Extract common terms (TF-IDF or simple word frequency)
|
||||
b. Sort by recency (most recent issue first)
|
||||
c. Report cluster with: theme, member issues, time span
|
||||
```
|
||||
|
||||
### Similarity Threshold Tuning
|
||||
|
||||
This is the critical parameter. Too low = noise, too high = misses.
|
||||
- Start at 0.80 cosine similarity
|
||||
- Expose as `--threshold` flag for user tuning
|
||||
- Report cluster cohesion score for transparency
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Recurring Patterns (3+ similar closed issues)
|
||||
|
||||
Cluster 1: "Authentication timeout errors" (5 issues, spanning 6 months)
|
||||
#89 Login timeout on slow networks (closed 3d ago)
|
||||
#72 Auth flow hangs on cellular (closed 2mo ago)
|
||||
#58 Token refresh timeout (closed 3mo ago)
|
||||
#45 SSO login timeout for remote users (closed 5mo ago)
|
||||
#31 Connection timeout in auth middleware (closed 6mo ago)
|
||||
Avg similarity: 0.87 | Suggested: systemic fix for auth timeout handling
|
||||
|
||||
Cluster 2: "Cache invalidation issues" (3 issues, spanning 2 months)
|
||||
#85 Stale cache after deploy (closed 2w ago)
|
||||
#77 Cache headers not updated (closed 1mo ago)
|
||||
#69 Dashboard shows old data after settings change (closed 2mo ago)
|
||||
Avg similarity: 0.82 | Suggested: review cache invalidation strategy
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Clustering quality depends on embedding quality and threshold tuning
|
||||
- May produce false clusters (issues that mention similar terms but are different problems)
|
||||
- Computationally expensive for large issue counts (N^2 comparisons)
|
||||
- Need to handle multi-chunk documents (aggregate embeddings)
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore recurring-patterns --open` — find clusters in open issues (duplicates to merge)
|
||||
- `lore recurring-patterns --cross-project` — patterns across repos
|
||||
- Trend detection: are cluster sizes growing? (escalating problem)
|
||||
- Export as report for engineering retrospectives
|
||||
78
docs/ideas/review-coverage.md
Normal file
78
docs/ideas/review-coverage.md
Normal file
@@ -0,0 +1,78 @@
|
||||
# DiffNote Coverage Map
|
||||
|
||||
- **Command:** `lore review-coverage <mr-iid>`
|
||||
- **Confidence:** 75%
|
||||
- **Tier:** 3
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — join DiffNote positions with mr_file_changes
|
||||
|
||||
## What
|
||||
|
||||
For a specific MR, show which files received review comments (DiffNotes) vs. which
|
||||
files were changed but received no review attention. Highlights blind spots in code
|
||||
review.
|
||||
|
||||
## Why
|
||||
|
||||
Large MRs often have files that get reviewed thoroughly and files that slip through
|
||||
with no comments. This makes the review coverage visible so teams can decide if
|
||||
un-reviewed files need a second look.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `mr_file_changes` (new_path per MR)
|
||||
- `notes` (position_new_path, note_type='DiffNote', discussion_id)
|
||||
- `discussions` (merge_request_id)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
mfc.new_path,
|
||||
mfc.change_type,
|
||||
COUNT(DISTINCT n.id) as review_comments,
|
||||
COUNT(DISTINCT d.id) as review_threads,
|
||||
CASE WHEN COUNT(n.id) = 0 THEN 'NOT REVIEWED' ELSE 'REVIEWED' END as status
|
||||
FROM mr_file_changes mfc
|
||||
LEFT JOIN notes n ON n.position_new_path = mfc.new_path
|
||||
AND n.note_type = 'DiffNote'
|
||||
AND n.is_system = 0
|
||||
LEFT JOIN discussions d ON n.discussion_id = d.id
|
||||
AND d.merge_request_id = mfc.merge_request_id
|
||||
WHERE mfc.merge_request_id = ?1
|
||||
GROUP BY mfc.new_path
|
||||
ORDER BY review_comments DESC;
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Review Coverage for !234 — Refactor auth middleware
|
||||
|
||||
REVIEWED (5 files, 23 comments)
|
||||
src/auth/middleware.rs 12 comments, 4 threads
|
||||
src/auth/jwt.rs 6 comments, 2 threads
|
||||
src/auth/session.rs 3 comments, 1 thread
|
||||
tests/auth/middleware_test.rs 1 comment, 1 thread
|
||||
src/auth/mod.rs 1 comment, 1 thread
|
||||
|
||||
NOT REVIEWED (3 files)
|
||||
src/auth/types.rs modified [no review comments]
|
||||
src/api/routes.rs modified [no review comments]
|
||||
Cargo.toml modified [no review comments]
|
||||
|
||||
Coverage: 5/8 files (62.5%)
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Reviewers may have reviewed a file without leaving comments (approval by silence)
|
||||
- position_new_path matching may not cover all DiffNote position formats
|
||||
- Config files (Cargo.toml) not being reviewed is usually fine
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore review-coverage --all --since 30d` — aggregate coverage across all MRs
|
||||
- Per-reviewer breakdown: which reviewers cover which files?
|
||||
- Coverage heatmap: files that consistently escape review across multiple MRs
|
||||
90
docs/ideas/silos.md
Normal file
90
docs/ideas/silos.md
Normal file
@@ -0,0 +1,90 @@
|
||||
# Knowledge Silo Detection
|
||||
|
||||
- **Command:** `lore silos [--min-changes <N>]`
|
||||
- **Confidence:** 87%
|
||||
- **Tier:** 2
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — requires mr_file_changes population (Gate 4)
|
||||
|
||||
## What
|
||||
|
||||
For each file path (or directory), count unique MR authors. Flag paths where only
|
||||
1 person has ever authored changes (bus factor = 1). Aggregate by directory to show
|
||||
silo areas.
|
||||
|
||||
## Why
|
||||
|
||||
Bus factor analysis is critical for team resilience. If only one person has ever
|
||||
touched the auth module, that's a risk. This uses data already ingested to surface
|
||||
knowledge concentration that's otherwise invisible.
|
||||
|
||||
## Data Required
|
||||
|
||||
- `mr_file_changes` (new_path, merge_request_id) — needs Gate 4 ingestion
|
||||
- `merge_requests` (author_username, state='merged')
|
||||
- `projects` (path_with_namespace)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
-- Find directories with bus factor = 1
|
||||
WITH file_authors AS (
|
||||
SELECT
|
||||
mfc.new_path,
|
||||
mr.author_username,
|
||||
p.path_with_namespace,
|
||||
mfc.project_id
|
||||
FROM mr_file_changes mfc
|
||||
JOIN merge_requests mr ON mfc.merge_request_id = mr.id
|
||||
JOIN projects p ON mfc.project_id = p.id
|
||||
WHERE mr.state = 'merged'
|
||||
),
|
||||
directory_authors AS (
|
||||
SELECT
|
||||
project_id,
|
||||
path_with_namespace,
|
||||
-- Extract directory: everything before last '/'
|
||||
CASE
|
||||
WHEN INSTR(new_path, '/') > 0
|
||||
THEN SUBSTR(new_path, 1, LENGTH(new_path) - LENGTH(REPLACE(RTRIM(new_path, REPLACE(new_path, '/', '')), '', '')))
|
||||
ELSE '.'
|
||||
END as directory,
|
||||
COUNT(DISTINCT author_username) as unique_authors,
|
||||
COUNT(*) as total_changes,
|
||||
GROUP_CONCAT(DISTINCT author_username) as authors
|
||||
FROM file_authors
|
||||
GROUP BY project_id, directory
|
||||
)
|
||||
SELECT * FROM directory_authors
|
||||
WHERE unique_authors = 1
|
||||
AND total_changes >= ?1 -- min-changes threshold
|
||||
ORDER BY total_changes DESC;
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Knowledge Silos (bus factor = 1, min 3 changes)
|
||||
|
||||
group/backend
|
||||
src/auth/ alice (8 changes) HIGH RISK
|
||||
src/billing/ bob (5 changes) HIGH RISK
|
||||
src/utils/cache/ charlie (3 changes) MODERATE RISK
|
||||
|
||||
group/frontend
|
||||
src/admin/ dave (12 changes) HIGH RISK
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Historical authors may have left the team; needs recency weighting
|
||||
- Requires `mr_file_changes` to be populated (Gate 4)
|
||||
- Single-author directories may be intentional (ownership model)
|
||||
- Directory aggregation heuristic is imperfect for deep nesting
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore silos --since 180d` — only count recent activity
|
||||
- `lore silos --depth 2` — aggregate at directory depth N
|
||||
- Combine with `lore experts` to show both silos and experts in one view
|
||||
- Risk scoring: weight by directory size, change frequency, recency
|
||||
95
docs/ideas/similar-issues.md
Normal file
95
docs/ideas/similar-issues.md
Normal file
@@ -0,0 +1,95 @@
|
||||
# Similar Issues Finder
|
||||
|
||||
- **Command:** `lore similar <iid>`
|
||||
- **Confidence:** 95%
|
||||
- **Tier:** 1
|
||||
- **Status:** proposed
|
||||
- **Effort:** low — infrastructure exists, needs one new query path
|
||||
|
||||
## What
|
||||
|
||||
Given an issue IID, find the N most semantically similar issues using the existing
|
||||
vector embeddings. Show similarity score and overlapping keywords.
|
||||
|
||||
Can also work with MRs: `lore similar --mr <iid>`.
|
||||
|
||||
## Why
|
||||
|
||||
Duplicate detection is a constant problem on active projects. "Is this bug already
|
||||
filed?" becomes a one-liner. This is the most natural use of the embedding pipeline
|
||||
and the feature people expect when they hear "semantic search."
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `documents` table (source_type, source_id, content_text)
|
||||
- `embeddings` virtual table (768-dim vectors via sqlite-vec)
|
||||
- `embedding_metadata` (document_hash for staleness check)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```
|
||||
1. Resolve IID → issue.id → document.id (via source_type='issue', source_id)
|
||||
2. Look up embedding vector(s) for that document
|
||||
3. Query sqlite-vec for K nearest neighbors (K = limit * 2 for headroom)
|
||||
4. Filter to source_type='issue' (or 'merge_request' if --include-mrs)
|
||||
5. Exclude self
|
||||
6. Rank by cosine similarity
|
||||
7. Return top N with: iid, title, project, similarity_score, url
|
||||
```
|
||||
|
||||
### SQL Core
|
||||
|
||||
```sql
|
||||
-- Get the embedding for target document (chunk 0 = representative)
|
||||
SELECT embedding FROM embeddings WHERE rowid = ?1 * 1000;
|
||||
|
||||
-- Find nearest neighbors
|
||||
SELECT
|
||||
rowid,
|
||||
distance
|
||||
FROM embeddings
|
||||
WHERE embedding MATCH ?1
|
||||
AND k = ?2
|
||||
ORDER BY distance;
|
||||
|
||||
-- Resolve back to entities
|
||||
SELECT d.source_type, d.source_id, d.title, d.url, i.iid, i.state
|
||||
FROM documents d
|
||||
JOIN issues i ON d.source_id = i.id AND d.source_type = 'issue'
|
||||
WHERE d.id = ?;
|
||||
```
|
||||
|
||||
## Robot Mode Output
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"query_issue": { "iid": 42, "title": "Login timeout on slow networks" },
|
||||
"similar": [
|
||||
{
|
||||
"iid": 38,
|
||||
"title": "Connection timeout in auth flow",
|
||||
"project": "group/backend",
|
||||
"similarity": 0.87,
|
||||
"state": "closed",
|
||||
"url": "https://gitlab.com/group/backend/-/issues/38"
|
||||
}
|
||||
]
|
||||
},
|
||||
"meta": { "elapsed_ms": 45, "candidates_scanned": 200 }
|
||||
}
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Embedding quality depends on description quality; short issues may not match well
|
||||
- Multi-chunk documents need aggregation strategy (use chunk 0 or average?)
|
||||
- Requires embeddings to be generated first (`lore embed`)
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore similar --open-only` to filter to unresolved issues (duplicate triage)
|
||||
- `lore similar --text "free text query"` to find issues similar to arbitrary text
|
||||
- Batch mode: find all potential duplicate clusters across the entire database
|
||||
100
docs/ideas/stale-discussions.md
Normal file
100
docs/ideas/stale-discussions.md
Normal file
@@ -0,0 +1,100 @@
|
||||
# Stale Discussion Finder
|
||||
|
||||
- **Command:** `lore stale-discussions [--days <N>]`
|
||||
- **Confidence:** 90%
|
||||
- **Tier:** 1
|
||||
- **Status:** proposed
|
||||
- **Effort:** low — single query, minimal formatting
|
||||
|
||||
## What
|
||||
|
||||
List unresolved, resolvable discussions where `last_note_at` is older than a
|
||||
threshold (default 14 days), grouped by parent entity. Prioritize by discussion
|
||||
count per entity (more stale threads = more urgent).
|
||||
|
||||
## Why
|
||||
|
||||
Unresolved discussions are silent blockers. They prevent MR merges, stall
|
||||
decision-making, and represent forgotten conversations. This surfaces them so teams
|
||||
can take action: resolve, respond, or explicitly mark as won't-fix.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `discussions` (resolved, resolvable, last_note_at)
|
||||
- `issues` / `merge_requests` (for parent entity context)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
SELECT
|
||||
d.id,
|
||||
d.noteable_type,
|
||||
CASE WHEN d.issue_id IS NOT NULL THEN i.iid ELSE mr.iid END as entity_iid,
|
||||
CASE WHEN d.issue_id IS NOT NULL THEN i.title ELSE mr.title END as entity_title,
|
||||
p.path_with_namespace,
|
||||
d.last_note_at,
|
||||
((?1 - d.last_note_at) / 86400000) as days_stale,
|
||||
COUNT(*) OVER (PARTITION BY COALESCE(d.issue_id, d.merge_request_id), d.noteable_type) as stale_count_for_entity
|
||||
FROM discussions d
|
||||
JOIN projects p ON d.project_id = p.id
|
||||
LEFT JOIN issues i ON d.issue_id = i.id
|
||||
LEFT JOIN merge_requests mr ON d.merge_request_id = mr.id
|
||||
WHERE d.resolved = 0
|
||||
AND d.resolvable = 1
|
||||
AND d.last_note_at < ?1
|
||||
ORDER BY days_stale DESC;
|
||||
```
|
||||
|
||||
## Human Output Format
|
||||
|
||||
```
|
||||
Stale Discussions (14+ days without activity)
|
||||
|
||||
group/backend !234 — Refactor auth middleware (3 stale threads)
|
||||
Discussion #a1b2c3 (28d stale) "Should we use JWT or session tokens?"
|
||||
Discussion #d4e5f6 (21d stale) "Error handling for expired tokens"
|
||||
Discussion #g7h8i9 (14d stale) "Performance implications of per-request validation"
|
||||
|
||||
group/backend #90 — Rate limiting design (1 stale thread)
|
||||
Discussion #j0k1l2 (18d stale) "Redis vs in-memory rate counter"
|
||||
```
|
||||
|
||||
## Robot Mode Output
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"threshold_days": 14,
|
||||
"total_stale": 4,
|
||||
"entities": [
|
||||
{
|
||||
"type": "merge_request",
|
||||
"iid": 234,
|
||||
"title": "Refactor auth middleware",
|
||||
"project": "group/backend",
|
||||
"stale_discussions": [
|
||||
{
|
||||
"discussion_id": "a1b2c3",
|
||||
"days_stale": 28,
|
||||
"first_note_preview": "Should we use JWT or session tokens?"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Some discussions are intentionally left open (design docs, long-running threads)
|
||||
- Could produce noise in repos with loose discussion hygiene
|
||||
- Doesn't distinguish "stale and blocking" from "stale and irrelevant"
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore stale-discussions --mr-only` — focus on MR review threads (most actionable)
|
||||
- `lore stale-discussions --author alice` — "threads I started that went quiet"
|
||||
- `lore stale-discussions --assignee bob` — "threads on my MRs that need attention"
|
||||
82
docs/ideas/unlinked.md
Normal file
82
docs/ideas/unlinked.md
Normal file
@@ -0,0 +1,82 @@
|
||||
# Unlinked MR Finder
|
||||
|
||||
- **Command:** `lore unlinked [--since <date>]`
|
||||
- **Confidence:** 83%
|
||||
- **Tier:** 2
|
||||
- **Status:** proposed
|
||||
- **Effort:** low — LEFT JOIN queries
|
||||
|
||||
## What
|
||||
|
||||
Two reports:
|
||||
1. Merged MRs with no entity_references at all (no "closes", no "mentioned",
|
||||
no "related") — orphan MRs with no issue traceability
|
||||
2. Closed issues with no MR reference — issues closed manually without code change
|
||||
|
||||
## Why
|
||||
|
||||
Process compliance metric. Unlinked MRs mean lost traceability — you can't trace
|
||||
a code change back to a requirement. Manually closed issues might mean work was done
|
||||
outside the tracked process, or issues were closed prematurely.
|
||||
|
||||
## Data Required
|
||||
|
||||
All exists today:
|
||||
- `merge_requests` (state, merged_at)
|
||||
- `issues` (state, closed/updated_at)
|
||||
- `entity_references` (for join/anti-join)
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
```sql
|
||||
-- Orphan merged MRs (no references at all)
|
||||
SELECT mr.iid, mr.title, mr.author_username, mr.merged_at,
|
||||
p.path_with_namespace
|
||||
FROM merge_requests mr
|
||||
JOIN projects p ON mr.project_id = p.id
|
||||
LEFT JOIN entity_references er
|
||||
ON er.source_entity_type = 'merge_request' AND er.source_entity_id = mr.id
|
||||
WHERE mr.state = 'merged'
|
||||
AND mr.merged_at >= ?1
|
||||
AND er.id IS NULL
|
||||
ORDER BY mr.merged_at DESC;
|
||||
|
||||
-- Closed issues with no MR reference
|
||||
SELECT i.iid, i.title, i.author_username, i.updated_at,
|
||||
p.path_with_namespace
|
||||
FROM issues i
|
||||
JOIN projects p ON i.project_id = p.id
|
||||
LEFT JOIN entity_references er
|
||||
ON er.target_entity_type = 'issue' AND er.target_entity_id = i.id
|
||||
AND er.source_entity_type = 'merge_request'
|
||||
WHERE i.state = 'closed'
|
||||
AND i.updated_at >= ?1
|
||||
AND er.id IS NULL
|
||||
ORDER BY i.updated_at DESC;
|
||||
```
|
||||
|
||||
## Human Output
|
||||
|
||||
```
|
||||
Unlinked MRs (merged with no issue reference, last 30 days)
|
||||
|
||||
!245 Fix typo in README (alice, merged 2d ago)
|
||||
!239 Update CI pipeline (bob, merged 1w ago)
|
||||
!236 Bump dependency versions (charlie, merged 2w ago)
|
||||
|
||||
Orphan Closed Issues (closed without any MR, last 30 days)
|
||||
|
||||
#92 Update documentation for v2 (closed by dave, 3d ago)
|
||||
#88 Investigate memory usage (closed by eve, 2w ago)
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Some MRs legitimately don't reference issues (chores, CI fixes, dependency bumps)
|
||||
- Some issues are legitimately closed without code (questions, duplicates, won't-fix)
|
||||
- Noise level depends on team discipline
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore unlinked --ignore-labels "chore,ci"` — filter out expected orphans
|
||||
- Compliance score: % of MRs with issue links over time (trend metric)
|
||||
102
docs/ideas/weekly-digest.md
Normal file
102
docs/ideas/weekly-digest.md
Normal file
@@ -0,0 +1,102 @@
|
||||
# Weekly Digest Generator
|
||||
|
||||
- **Command:** `lore weekly [--since <date>]`
|
||||
- **Confidence:** 90%
|
||||
- **Tier:** 1
|
||||
- **Status:** proposed
|
||||
- **Effort:** medium — builds on digest infrastructure, adds markdown formatting
|
||||
|
||||
## What
|
||||
|
||||
Auto-generate a markdown document summarizing the week: MRs merged (grouped by
|
||||
project), issues closed, new issues opened, ongoing discussions, milestone progress.
|
||||
Formatted for pasting into Slack, email, or team standup notes.
|
||||
|
||||
Default window is 7 days. `--since` overrides.
|
||||
|
||||
## Why
|
||||
|
||||
Every team lead writes a weekly status update. This writes itself from the data.
|
||||
Leverages everything gitlore has ingested. Saves 30-60 minutes of manual summarization
|
||||
per week.
|
||||
|
||||
## Data Required
|
||||
|
||||
Same as digest (all exists today):
|
||||
- `resource_state_events`, `merge_requests`, `issues`, `discussions`
|
||||
- `milestones` for progress tracking
|
||||
|
||||
## Implementation Sketch
|
||||
|
||||
This is essentially `lore digest --since 7d --format markdown` with:
|
||||
1. Section headers for each category
|
||||
2. Milestone progress bars (X/Y issues closed)
|
||||
3. "Highlights" section with the most-discussed items
|
||||
4. "Risks" section with overdue issues and stale MRs
|
||||
|
||||
### Markdown Template
|
||||
|
||||
```markdown
|
||||
# Weekly Summary — Jan 20-27, 2025
|
||||
|
||||
## Highlights
|
||||
- **!234** Refactor auth middleware merged (12 discussions, 4 reviewers)
|
||||
- **#95** New critical bug: Rate limiting returns 500
|
||||
|
||||
## Merged (3)
|
||||
| MR | Title | Author | Reviewers |
|
||||
|----|-------|--------|-----------|
|
||||
| !234 | Refactor auth middleware | alice | bob, charlie |
|
||||
| !231 | Fix connection pool leak | bob | alice |
|
||||
| !45 | Update dashboard layout | eve | dave |
|
||||
|
||||
## Closed Issues (2)
|
||||
- **#89** Login timeout on slow networks (closed by alice)
|
||||
- **#87** Stale cache headers (closed by bob)
|
||||
|
||||
## New Issues (3)
|
||||
- **#95** Rate limiting returns 500 (priority::high, assigned to charlie)
|
||||
- **#94** Add rate limit documentation (priority::low)
|
||||
- **#93** Flaky test in CI pipeline (assigned to dave)
|
||||
|
||||
## Milestone Progress
|
||||
- **v2.0** — 14/20 issues closed (70%) — due Feb 15
|
||||
- **v1.9-hotfix** — 3/3 issues closed (100%) — COMPLETE
|
||||
|
||||
## Active Discussions
|
||||
- **#90** 8 new comments this week (needs-review)
|
||||
- **!230** 5 review threads unresolved
|
||||
```
|
||||
|
||||
## Robot Mode Output
|
||||
|
||||
```json
|
||||
{
|
||||
"ok": true,
|
||||
"data": {
|
||||
"period": { "from": "2025-01-20", "to": "2025-01-27" },
|
||||
"merged_count": 3,
|
||||
"closed_count": 2,
|
||||
"opened_count": 3,
|
||||
"highlights": [...],
|
||||
"merged": [...],
|
||||
"closed": [...],
|
||||
"opened": [...],
|
||||
"milestones": [...],
|
||||
"active_discussions": [...]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## Downsides
|
||||
|
||||
- Formatting preferences vary by team; hard to please everyone
|
||||
- "Highlights" ranking is heuristic (discussion count as proxy for importance)
|
||||
- Doesn't capture work done outside GitLab
|
||||
|
||||
## Extensions
|
||||
|
||||
- `lore weekly --project group/backend` — single project scope
|
||||
- `lore weekly --author alice` — personal weekly summary
|
||||
- `lore weekly --output weekly.md` — write to file
|
||||
- Scheduled generation via cron + robot mode
|
||||
140
docs/issues/001-timeline-missing-project-in-entity-ref.md
Normal file
140
docs/issues/001-timeline-missing-project-in-entity-ref.md
Normal file
@@ -0,0 +1,140 @@
|
||||
# 001: Timeline human output omits project path from entity references
|
||||
|
||||
- **Severity:** medium
|
||||
- **Component:** `src/cli/commands/timeline.rs`
|
||||
- **Status:** open
|
||||
|
||||
## Problem
|
||||
|
||||
The `lore timeline` human-readable output renders entity references as bare `#42` or
|
||||
`!234` without the project path. When multiple projects are synced, this makes the
|
||||
output ambiguous — issue `#42` in `group/backend` and `#42` in `group/frontend` are
|
||||
indistinguishable.
|
||||
|
||||
### Affected code
|
||||
|
||||
`format_entity_ref` at `src/cli/commands/timeline.rs:201-207`:
|
||||
|
||||
```rust
|
||||
fn format_entity_ref(entity_type: &str, iid: i64) -> String {
|
||||
match entity_type {
|
||||
"issue" => format!("#{iid}"),
|
||||
"merge_request" => format!("!{iid}"),
|
||||
_ => format!("{entity_type}:{iid}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
This function is called in three places:
|
||||
|
||||
1. **Event lines** (`print_timeline_event`, line 130) — each event row shows `#42`
|
||||
with no project context
|
||||
2. **Footer seed list** (`print_timeline_footer`, line 161) — seed entities listed as
|
||||
`#42, !234` with no project disambiguation
|
||||
3. **Collect stage summaries** (`timeline_collect.rs:107`) — the `summary` field itself
|
||||
bakes in `"Issue #42 created: ..."` without project
|
||||
|
||||
### Current output (ambiguous)
|
||||
|
||||
```
|
||||
2025-01-20 CREATED #42 Issue #42 created: Login timeout bug @alice
|
||||
2025-01-21 LABEL+ #42 Label added: priority::high @dave
|
||||
2025-01-22 CREATED !234 MR !234 created: Refactor auth middleware @alice
|
||||
2025-01-25 MERGED !234 MR !234 merged @bob
|
||||
|
||||
Seed entities: #42, !234
|
||||
```
|
||||
|
||||
When multiple projects are synced, a reader cannot tell which project `#42` belongs to.
|
||||
|
||||
## Robot mode is partially affected
|
||||
|
||||
The robot JSON output (`EventJson`, line 387-416) DOES include a `project` field per
|
||||
event, so programmatic consumers can disambiguate. However, the `summary` string field
|
||||
still bakes in bare `#42` without project context, which is misleading if an agent uses
|
||||
the summary for display.
|
||||
|
||||
## Proposed fix
|
||||
|
||||
### 1. Add project to `format_entity_ref`
|
||||
|
||||
Pass `project_path` into `format_entity_ref` and use GitLab's full reference format:
|
||||
|
||||
```rust
|
||||
fn format_entity_ref(entity_type: &str, iid: i64, project_path: &str) -> String {
|
||||
match entity_type {
|
||||
"issue" => format!("{project_path}#{iid}"),
|
||||
"merge_request" => format!("{project_path}!{iid}"),
|
||||
_ => format!("{project_path}/{entity_type}:{iid}"),
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Smart elision for single-project timelines
|
||||
|
||||
When all events belong to the same project, the full path is visual noise. Detect
|
||||
this and fall back to bare `#42` / `!234`:
|
||||
|
||||
```rust
|
||||
fn should_show_project(events: &[TimelineEvent]) -> bool {
|
||||
let mut projects = events.iter().map(|e| &e.project_path).collect::<HashSet<_>>();
|
||||
projects.len() > 1
|
||||
}
|
||||
```
|
||||
|
||||
Then conditionally format:
|
||||
|
||||
```rust
|
||||
let entity_ref = if show_project {
|
||||
format_entity_ref(&event.entity_type, event.entity_iid, &event.project_path)
|
||||
} else {
|
||||
format_entity_ref_short(&event.entity_type, event.entity_iid)
|
||||
};
|
||||
```
|
||||
|
||||
### 3. Fix summary strings in collect stage
|
||||
|
||||
`timeline_collect.rs:107` bakes the summary as `"Issue #42 created: title"`. This
|
||||
should include the project when multi-project:
|
||||
|
||||
```rust
|
||||
let prefix = if multi_project {
|
||||
format!("{type_label} {project_path}#{iid}")
|
||||
} else {
|
||||
format!("{type_label} #{iid}")
|
||||
};
|
||||
summary = format!("{prefix} created: {title_str}");
|
||||
```
|
||||
|
||||
Same pattern for the merge summary at lines 317 and 347.
|
||||
|
||||
### 4. Update footer seed list
|
||||
|
||||
`print_timeline_footer` (line 155-164) should also use the project-aware format:
|
||||
|
||||
```rust
|
||||
result.seed_entities.iter()
|
||||
.map(|e| format_entity_ref(&e.entity_type, e.entity_iid, &e.project_path))
|
||||
```
|
||||
|
||||
## Expected output after fix
|
||||
|
||||
### Single project (no change)
|
||||
|
||||
```
|
||||
2025-01-20 CREATED #42 Issue #42 created: Login timeout bug @alice
|
||||
```
|
||||
|
||||
### Multi-project (project path added)
|
||||
|
||||
```
|
||||
2025-01-20 CREATED group/backend#42 Issue group/backend#42 created: Login timeout @alice
|
||||
2025-01-22 CREATED group/frontend#42 Issue group/frontend#42 created: Broken layout @eve
|
||||
```
|
||||
|
||||
## Impact
|
||||
|
||||
- Human output: ambiguous for multi-project users (the primary use case for gitlore)
|
||||
- Robot output: summary field misleading, but `project` field provides workaround
|
||||
- Timeline footer: seed entity list ambiguous
|
||||
- Collect-stage summaries: baked-in bare references propagate to both renderers
|
||||
179
docs/performance-audit-2026-02-12.md
Normal file
179
docs/performance-audit-2026-02-12.md
Normal file
@@ -0,0 +1,179 @@
|
||||
# Deep Performance Audit Report
|
||||
|
||||
**Date:** 2026-02-12
|
||||
**Branch:** `perf-audit` (e9bacc94)
|
||||
**Parent:** `039ab1c2` (master, v0.6.1)
|
||||
|
||||
---
|
||||
|
||||
## Methodology
|
||||
|
||||
1. **Baseline** — measured p50/p95 latency for all major commands with warm cache
|
||||
2. **Profile** — used macOS `sample` profiler and `EXPLAIN QUERY PLAN` to identify hotspots
|
||||
3. **Golden output** — captured exact numeric outputs before changes as equivalence oracle
|
||||
4. **One lever per change** — each optimization isolated and independently benchmarked
|
||||
5. **Revert threshold** — any optimization <1.1x speedup reverted per audit rules
|
||||
|
||||
---
|
||||
|
||||
## Baseline Measurements (warm cache, release build)
|
||||
|
||||
| Command | Latency | Notes |
|
||||
|---------|---------|-------|
|
||||
| `who --path src/core/db.rs` (expert) | 2200ms | **Hotspot** |
|
||||
| `who --active` | 83-93ms | Acceptable |
|
||||
| `who workload` | 22ms | Fast |
|
||||
| `stats` | 107-112ms | **Hotspot** |
|
||||
| `search "authentication"` | 1030ms | **Hotspot** (library-level) |
|
||||
| `list issues -n 50` | ~40ms | Fast |
|
||||
|
||||
---
|
||||
|
||||
## Optimization 1: INDEXED BY for DiffNote Queries
|
||||
|
||||
**Target:** `src/cli/commands/who.rs` — expert and reviews query paths
|
||||
|
||||
**Problem:** SQLite query planner chose `idx_notes_system` (38% selectivity, 106K rows) over `idx_notes_diffnote_path_created` (9.3% selectivity, 26K rows) for path-filtered DiffNote queries. The partial index `WHERE noteable_type = 'MergeRequest' AND type = 'DiffNote'` is far more selective but the planner's cost model didn't pick it.
|
||||
|
||||
**Change:** Added `INDEXED BY idx_notes_diffnote_path_created` to all 8 SQL queries across `query_expert`, `query_expert_details`, `query_reviews`, `build_path_query` (probes 1 & 2), and `suffix_probe`.
|
||||
|
||||
**Results:**
|
||||
|
||||
| Query | Before | After | Speedup |
|
||||
|-------|--------|-------|---------|
|
||||
| expert (specific path) | 2200ms | 56-58ms | **38x** |
|
||||
| expert (broad path) | 2200ms | 83ms | **26x** |
|
||||
| reviews | 1800ms | 24ms | **75x** |
|
||||
|
||||
**Isomorphism proof:** `INDEXED BY` only changes which index the planner uses, not the query semantics. Same rows matched, same ordering, same output. Verified by golden output comparison across 5+ runs.
|
||||
|
||||
---
|
||||
|
||||
## Optimization 2: Conditional Aggregates in Stats
|
||||
|
||||
**Target:** `src/cli/commands/stats.rs`
|
||||
|
||||
**Problem:** 12+ sequential `COUNT(*)` queries each requiring a full table scan of `documents` (61K rows). Each scan touched the same pages but couldn't share work.
|
||||
|
||||
**Changes:**
|
||||
- Documents: 5 sequential COUNTs -> 1 query with `SUM(CASE WHEN ... THEN 1 END)`
|
||||
- FTS count: `SELECT COUNT(*) FROM documents_fts` (virtual table, slow) -> `SELECT COUNT(*) FROM documents_fts_docsize` (shadow B-tree table, 19x faster)
|
||||
- Embeddings: 2 queries -> 1 with `COUNT(DISTINCT document_id), COUNT(*)`
|
||||
- Dirty sources: 2 queries -> 1 with conditional aggregates
|
||||
- Pending fetches: 2 queries -> 1 each (discussions, dependents)
|
||||
|
||||
**Results:**
|
||||
|
||||
| Metric | Before | After | Speedup |
|
||||
|--------|--------|-------|---------|
|
||||
| Warm median | 112ms | 66ms | **1.70x** |
|
||||
| Cold | 1220ms | ~700ms | ~1.7x |
|
||||
|
||||
**Golden output verified:**
|
||||
|
||||
```
|
||||
total:61652, issues:8241, mrs:10018, discussions:43393, truncated:63
|
||||
fts:61652, embedded:61652, chunks:88161
|
||||
```
|
||||
|
||||
All values match exactly across before/after runs.
|
||||
|
||||
**Isomorphism proof:** `SUM(CASE WHEN x THEN 1 END)` is algebraically identical to `COUNT(*) WHERE x`. The FTS5 shadow table `documents_fts_docsize` has exactly one row per FTS document by SQLite specification, so `COUNT(*)` on it equals the virtual table count.
|
||||
|
||||
---
|
||||
|
||||
## Investigation: Two-Phase FTS Search (REVERTED)
|
||||
|
||||
**Target:** `src/search/fts.rs`, `src/cli/commands/search.rs`
|
||||
|
||||
**Hypothesis:** FTS5 `snippet()` generation is expensive. Splitting search into Phase 1 (score-only MATCH+bm25) and Phase 2 (snippet for filtered results only) should reduce work.
|
||||
|
||||
**Implementation:** Created `fetch_fts_snippets()` that retrieves snippets only for post-filter document IDs via `json_each()` join.
|
||||
|
||||
**Results:**
|
||||
|
||||
| Metric | Before | After | Improvement |
|
||||
|--------|--------|-------|-------------|
|
||||
| search (limit 20) | 1030ms | 995ms | 3.5% |
|
||||
|
||||
**Decision:** Reverted. Per audit rules, <1.1x speedup does not justify added code complexity.
|
||||
|
||||
**Root cause:** The bottleneck is not snippet generation but `MATCH` + `bm25()` scoring itself. Profiling showed `strspn` (FTS5 tokenizer) and `memmove` as the top CPU consumers. The same query runs in 30ms on system sqlite3 but 1030ms in rusqlite's bundled SQLite — a ~125x gap despite both being SQLite 3.51.x compiled at -O3.
|
||||
|
||||
---
|
||||
|
||||
## Library-Level Finding: Bundled SQLite FTS5 Performance
|
||||
|
||||
**Observation:** FTS5 MATCH+bm25 queries are ~125x slower in rusqlite's bundled SQLite vs system sqlite3.
|
||||
|
||||
| Environment | Query Time | Notes |
|
||||
|-------------|-----------|-------|
|
||||
| System sqlite3 (macOS) | 30ms (with snippet), 8ms (without) | Same .db file |
|
||||
| rusqlite bundled | 1030ms | `features = ["bundled"]`, OPT_LEVEL=3 |
|
||||
|
||||
**Profiler data (macOS `sample`):**
|
||||
- Top hotspot: `strspn` in FTS5 tokenizer
|
||||
- Secondary: `memmove` in FTS5 internals
|
||||
- Scaling: ~5ms per result (limit 5 = 497ms, limit 20 = 995ms)
|
||||
|
||||
**Possible causes:**
|
||||
- Bundled SQLite compiled without platform-specific optimizations (SIMD, etc.)
|
||||
- Different memory allocator behavior
|
||||
- Missing compile-time tuning flags
|
||||
|
||||
**Recommendation for future:** Investigate switching from `features = ["bundled"]` to system SQLite linkage, or audit the bundled compile flags in the `libsqlite3-sys` build script.
|
||||
|
||||
---
|
||||
|
||||
## Exploration Agent Findings (Informational)
|
||||
|
||||
Four parallel exploration agents surveyed the entire codebase. Key findings beyond what was already addressed:
|
||||
|
||||
### Ingestion Pipeline
|
||||
- Serial DB writes in async context (acceptable — rusqlite is synchronous)
|
||||
- Label ingestion uses individual inserts (potential batch optimization, low priority)
|
||||
|
||||
### CLI / GitLab Client
|
||||
- GraphQL client recreated per call (`client.rs:98-100`) — caches connection pool, minor
|
||||
- Double JSON deserialization in GraphQL responses — medium priority
|
||||
- N+1 subqueries in `list` command (`list.rs:408-423`) — 4 correlated subqueries per row
|
||||
|
||||
### Search / Embedding
|
||||
- No N+1 patterns, no O(n^2) algorithms
|
||||
- Chunking is O(n) single-pass with proper UTF-8 safety
|
||||
- Ollama concurrency model is sound (parallel HTTP, serial DB writes)
|
||||
|
||||
### Database / Documents
|
||||
- O(n^2) prefix sum in `truncation.rs` — low traffic path
|
||||
- String allocation patterns in extractors — micro-optimization territory
|
||||
|
||||
---
|
||||
|
||||
## Opportunity Matrix
|
||||
|
||||
| Candidate | Impact | Confidence | Effort | Score | Status |
|
||||
|-----------|--------|------------|--------|-------|--------|
|
||||
| INDEXED BY for DiffNote | Very High | High | Low | **9.0** | Shipped |
|
||||
| Stats conditional aggregates | Medium | High | Low | **7.0** | Shipped |
|
||||
| Bundled SQLite FTS5 | Very High | Medium | High | 5.0 | Documented |
|
||||
| List N+1 subqueries | Medium | Medium | Medium | 4.0 | Backlog |
|
||||
| GraphQL double deser | Low | Medium | Low | 3.5 | Backlog |
|
||||
| Truncation O(n^2) | Low | High | Low | 3.0 | Backlog |
|
||||
|
||||
---
|
||||
|
||||
## Files Modified
|
||||
|
||||
| File | Change |
|
||||
|------|--------|
|
||||
| `src/cli/commands/who.rs` | INDEXED BY hints on 8 SQL queries |
|
||||
| `src/cli/commands/stats.rs` | Conditional aggregates, FTS5 shadow table, merged queries |
|
||||
|
||||
---
|
||||
|
||||
## Quality Gates
|
||||
|
||||
- All 603 tests pass
|
||||
- `cargo clippy --all-targets -- -D warnings` clean
|
||||
- `cargo fmt --check` clean
|
||||
- Golden output verified for both optimizations
|
||||
174
docs/prd-per-note-search.feedback-1.md
Normal file
174
docs/prd-per-note-search.feedback-1.md
Normal file
@@ -0,0 +1,174 @@
|
||||
Highest-impact gaps I see in the current plan:
|
||||
|
||||
1. `for-issue` / `for-mr` filtering is ambiguous across projects and can return incorrect rows.
|
||||
2. `lore notes` has no pagination contract, so large exports and deterministic resumption are weak.
|
||||
3. Migration `022` is high-risk (table rebuild + FTS + junction tables) without explicit integrity gates.
|
||||
4. Note-doc freshness is incomplete for upstream note deletions and parent metadata changes (labels/title).
|
||||
|
||||
Below are my best revisions, each with rationale and a git-diff-style plan edit.
|
||||
|
||||
---
|
||||
|
||||
1. **Add gated rollout + rollback controls**
|
||||
Rationale: You can still “ship together” while reducing blast radius. This makes recovery fast if note-doc generation causes DB/embedding pressure.
|
||||
|
||||
```diff
|
||||
@@ ## Design
|
||||
-Two phases, shipped together as one feature:
|
||||
+Two phases, shipped together as one feature, but with runtime gates:
|
||||
+
|
||||
+- `feature.notes_cli` (Phase 1 surface)
|
||||
+- `feature.note_documents` (Phase 2 indexing/extraction path)
|
||||
+
|
||||
+Rollout order:
|
||||
+1) Enable `notes_cli`
|
||||
+2) Run note-doc backfill in bounded batches
|
||||
+3) Enable `note_documents` for continuous updates
|
||||
+
|
||||
+Rollback:
|
||||
+- Disabling `feature.note_documents` stops new note-doc generation without affecting issue/MR/discussion docs.
|
||||
```
|
||||
|
||||
2. **Add keyset pagination + deterministic ordering**
|
||||
Rationale: Needed for year-long reviewer analysis and reliable “continue where I left off” behavior under concurrent updates.
|
||||
|
||||
```diff
|
||||
@@ pub struct NoteListFilters<'a> {
|
||||
pub limit: usize,
|
||||
+ pub cursor: Option<&'a str>, // keyset token "<sort_ms>:<id>"
|
||||
+ pub include_total_count: bool, // avoid COUNT(*) in hot paths
|
||||
@@
|
||||
- pub sort: &'a str, // "created" (default) | "updated"
|
||||
+ pub sort: &'a str, // "created" | "updated"
|
||||
@@ query_notes SQL
|
||||
-ORDER BY {sort_column} {order}
|
||||
+ORDER BY {sort_column} {order}, n.id {order}
|
||||
LIMIT ?
|
||||
```
|
||||
|
||||
3. **Make `for-issue` / `for-mr` project-scoped**
|
||||
Rationale: IIDs are not globally unique. Requiring project avoids false positives and hard-to-debug cross-project leakage.
|
||||
|
||||
```diff
|
||||
@@ pub struct NotesArgs {
|
||||
- #[arg(long = "for-issue", help_heading = "Filters", conflicts_with = "for_mr")]
|
||||
+ #[arg(long = "for-issue", help_heading = "Filters", conflicts_with = "for_mr", requires = "project")]
|
||||
pub for_issue: Option<i64>,
|
||||
@@
|
||||
- #[arg(long = "for-mr", help_heading = "Filters", conflicts_with = "for_issue")]
|
||||
+ #[arg(long = "for-mr", help_heading = "Filters", conflicts_with = "for_issue", requires = "project")]
|
||||
pub for_mr: Option<i64>,
|
||||
```
|
||||
|
||||
4. **Upgrade path filtering semantics**
|
||||
Rationale: Review comments often reference renames/moves. Restricting to `position_new_path` misses relevant notes.
|
||||
|
||||
```diff
|
||||
@@ pub struct NotesArgs {
|
||||
- /// Filter by file path (trailing / for prefix match)
|
||||
+ /// Filter by file path
|
||||
#[arg(long, help_heading = "Filters")]
|
||||
pub path: Option<String>,
|
||||
+ /// Path mode: exact|prefix|glob
|
||||
+ #[arg(long = "path-mode", value_parser = ["exact","prefix","glob"], default_value = "exact", help_heading = "Filters")]
|
||||
+ pub path_mode: String,
|
||||
+ /// Match against old path as well as new path
|
||||
+ #[arg(long = "match-old-path", help_heading = "Filters")]
|
||||
+ pub match_old_path: bool,
|
||||
@@ query_notes filter mappings
|
||||
-- `path` ... n.position_new_path ...
|
||||
+- `path` applies to `n.position_new_path` and optionally `n.position_old_path`.
|
||||
+- `glob` mode translates `*`/`?` to SQL LIKE with escaping.
|
||||
```
|
||||
|
||||
5. **Add explicit performance indexes (new migration)**
|
||||
Rationale: `notes` becomes a first-class query surface; without indexes, filters degrade quickly at 10k+ note scale.
|
||||
|
||||
```diff
|
||||
@@ ## Phase 1: `lore notes` Command
|
||||
+### Work Chunk 1E: Query Performance Indexes
|
||||
+**Files:** `migrations/023_notes_query_indexes.sql`, `src/core/db.rs`
|
||||
+
|
||||
+Add indexes:
|
||||
+- `notes(project_id, created_at DESC, id DESC)`
|
||||
+- `notes(author_username, created_at DESC, id DESC) WHERE is_system = 0`
|
||||
+- `notes(discussion_id)`
|
||||
+- `notes(position_new_path)`
|
||||
+- `notes(position_old_path)`
|
||||
+- `discussions(issue_id)`
|
||||
+- `discussions(merge_request_id)`
|
||||
```
|
||||
|
||||
6. **Harden migration 022 with transactional integrity checks**
|
||||
Rationale: This is the riskiest part of the plan. Add hard fail-fast checks so corruption cannot silently pass.
|
||||
|
||||
```diff
|
||||
@@ ### Work Chunk 2A: Schema Migration (022)
|
||||
+Migration safety requirements:
|
||||
+- Execute in a single `BEGIN IMMEDIATE ... COMMIT` transaction.
|
||||
+- Capture and compare pre/post row counts for `documents`, `document_labels`, `document_paths`, `dirty_sources`.
|
||||
+- Run `PRAGMA foreign_key_check` and abort on any violation.
|
||||
+- Run `PRAGMA integrity_check` and abort on non-`ok`.
|
||||
+- Rebuild FTS and assert `documents_fts` rowcount equals `documents` rowcount.
|
||||
```
|
||||
|
||||
7. **Add note deletion + parent-change propagation**
|
||||
Rationale: Current plan handles create/update ingestion but not all staleness paths. Without this, note documents drift.
|
||||
|
||||
```diff
|
||||
@@ ## Phase 2: Per-Note Documents
|
||||
+### Work Chunk 2G: Freshness Propagation
|
||||
+**Files:** `src/ingestion/discussions.rs`, `src/ingestion/mr_discussions.rs`, `src/documents/regenerator.rs`
|
||||
+
|
||||
+Rules:
|
||||
+- If a previously stored note is missing from upstream payload, delete local note row and enqueue `(note, id)` for document deletion.
|
||||
+- When parent issue/MR title or labels change, enqueue descendant note docs dirty (notes inherit parent metadata).
|
||||
+- Keep idempotent behavior for repeated syncs.
|
||||
```
|
||||
|
||||
8. **Separate FTS coverage from embedding coverage**
|
||||
Rationale: Biggest cost/perf risk is embeddings. Index all notes in FTS, but embed selectively with policy knobs.
|
||||
|
||||
```diff
|
||||
@@ ## Estimated Document Volume Impact
|
||||
-FTS5 handles this comfortably. Embedding generation time scales linearly (~4x increase).
|
||||
+FTS5 handles this comfortably. Embedding generation is policy-controlled:
|
||||
+- FTS: index all non-system note docs
|
||||
+- Embeddings default: only notes with body length >= 40 chars (configurable)
|
||||
+- Add config: `documents.note_embeddings.min_chars`, `documents.note_embeddings.enabled`
|
||||
+- Prioritize unresolved DiffNotes before other notes during embedding backfill
|
||||
```
|
||||
|
||||
9. **Bring structured reviewer profiling into scope (not narrative reporting)**
|
||||
Rationale: This directly serves the stated use case and makes the feature compelling immediately.
|
||||
|
||||
```diff
|
||||
@@ ## Non-Goals
|
||||
-- Adding a "reviewer profile" report command (that's a downstream use case built on this infrastructure)
|
||||
+- Generating free-form narrative reviewer reports.
|
||||
+ A structured profiling command is in scope.
|
||||
+
|
||||
+## Phase 3: Structured Reviewer Profiling
|
||||
+Add `lore notes profile --author <user> --since <window>` returning:
|
||||
+- top commented paths
|
||||
+- top parent labels
|
||||
+- unresolved-comment ratio
|
||||
+- note-type distribution
|
||||
+- median comment length
|
||||
```
|
||||
|
||||
10. **Add operational SLOs + robot-mode status for note pipeline**
|
||||
Rationale: Reliability improves when regressions are observable, not inferred from failures.
|
||||
|
||||
```diff
|
||||
@@ ## Verification Checklist
|
||||
+Operational checks:
|
||||
+- `lore -J stats` includes per-`source_type` document counts (including `note`)
|
||||
+- Add queue lag metrics: oldest dirty note age, retry backlog size
|
||||
+- Add extraction error breakdown by `source_type`
|
||||
+- Add smoke assertion: disabling `feature.note_documents` leaves other source regeneration unaffected
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If you want, I can produce a single consolidated revised PRD draft (fully merged text, not just diffs) as the next step.
|
||||
200
docs/prd-per-note-search.feedback-2.md
Normal file
200
docs/prd-per-note-search.feedback-2.md
Normal file
@@ -0,0 +1,200 @@
|
||||
Below are the strongest revisions I’d make, excluding everything in your `## Rejected Recommendations` list.
|
||||
|
||||
1. **Add a Phase 0 for stable note identity before any note-doc generation**
|
||||
Rationale: your current plan still allows note document churn because Issue discussion ingestion is delete/reinsert-based. That makes local `notes.id` unstable, causing unnecessary dirtying/regeneration and potential stale-doc edge cases. Stabilizing identity first (upsert-by-GitLab-ID + sweep stale) improves correctness and cuts repeated work.
|
||||
|
||||
```diff
|
||||
@@ ## Design
|
||||
-Two phases, shipped together as one feature:
|
||||
+Three phases, shipped together as one feature:
|
||||
+- **Phase 0 (Foundation):** Stable note identity in local DB (upsert + sweep, no delete/reinsert churn)
|
||||
- **Phase 1 (Option A):** `lore notes` command — direct SQL query over the `notes` table with rich filtering
|
||||
- **Phase 2 (Option B):** Per-note documents — each non-system note becomes its own searchable document in the FTS/embedding pipeline
|
||||
@@
|
||||
+## Phase 0: Stable Note Identity
|
||||
+
|
||||
+### Work Chunk 0A: Upsert/Sweep for Issue Discussion Notes
|
||||
+**Files:** `src/ingestion/discussions.rs`, `migrations/022_notes_identity_index.sql`, `src/core/db.rs`
|
||||
+**Implementation:**
|
||||
+- Add unique index: `UNIQUE(project_id, gitlab_id)` on `notes`
|
||||
+- Replace delete/reinsert issue-note flow with upsert + `last_seen_at` sweep (same durability model as MR note sweep)
|
||||
+- Ensure `insert_note/upsert_note` returns the stable local row id for both insert and update paths
|
||||
```
|
||||
|
||||
2. **Replace `source_type` CHECK constraints with a registry table + FK in migration**
|
||||
Rationale: table CHECKs force full table rebuild for every new source type forever. A `source_types` table with FK keeps DB-level integrity and future extensibility without rebuilding `documents`/`dirty_sources` every time. This is a major architecture hardening win.
|
||||
|
||||
```diff
|
||||
@@ ### Work Chunk 2A: Schema Migration (023)
|
||||
-Current migration ... CHECK constraints limiting `source_type` ...
|
||||
+Current migration ... CHECK constraints limiting `source_type` ...
|
||||
+Revision: migrate to `source_types` registry table + FK constraints.
|
||||
@@
|
||||
-1. `dirty_sources` — add `'note'` to source_type CHECK
|
||||
-2. `documents` — add `'note'` to source_type CHECK
|
||||
+1. Create `source_types(name TEXT PRIMARY KEY)` and seed: `issue, merge_request, discussion, note`
|
||||
+2. Rebuild `dirty_sources` and `documents` to replace CHECK with `REFERENCES source_types(name)`
|
||||
+3. Future source-type additions become `INSERT INTO source_types(name) VALUES (?)` (no table rebuild)
|
||||
@@
|
||||
+#### Additional integrity tests
|
||||
+#[test]
|
||||
+fn test_source_types_registry_contains_note() { ... }
|
||||
+#[test]
|
||||
+fn test_documents_source_type_fk_enforced() { ... }
|
||||
+#[test]
|
||||
+fn test_dirty_sources_source_type_fk_enforced() { ... }
|
||||
```
|
||||
|
||||
3. **Mark note documents dirty only when note semantics actually changed**
|
||||
Rationale: current loops mark every non-system note dirty every sync. With 8k+ notes this creates avoidable queue pressure and regeneration time. Change-aware dirtying (inserted/changed only) gives major performance and stability improvements.
|
||||
|
||||
```diff
|
||||
@@ ### Work Chunk 2D: Regenerator & Dirty Tracking Integration
|
||||
-for note in notes {
|
||||
- let local_note_id = insert_note(&tx, local_discussion_id, ¬e, None)?;
|
||||
- if !note.is_system {
|
||||
- dirty_tracker::mark_dirty_tx(&tx, SourceType::Note, local_note_id)?;
|
||||
- }
|
||||
-}
|
||||
+for note in notes {
|
||||
+ let outcome = upsert_note(&tx, local_discussion_id, ¬e, None)?;
|
||||
+ if !note.is_system && outcome.changed_semantics {
|
||||
+ dirty_tracker::mark_dirty_tx(&tx, SourceType::Note, outcome.local_note_id)?;
|
||||
+ }
|
||||
+}
|
||||
@@
|
||||
+// changed_semantics should include: body, note_type, path/line positions, resolvable/resolved/resolved_by, updated_at
|
||||
```
|
||||
|
||||
4. **Expand filters to support real analysis windows and resolution state**
|
||||
Rationale: reviewer profiling usually needs bounded windows and both resolved/unresolved views. Current `unresolved: bool` is too narrow and one-sided. Add `--until` and tri-state resolution filtering for better analytical power.
|
||||
|
||||
```diff
|
||||
@@ pub struct NoteListFilters<'a> {
|
||||
- pub since: Option<&'a str>,
|
||||
+ pub since: Option<&'a str>,
|
||||
+ pub until: Option<&'a str>,
|
||||
@@
|
||||
- pub unresolved: bool,
|
||||
+ pub resolution: &'a str, // "any" (default) | "unresolved" | "resolved"
|
||||
@@
|
||||
- pub author: Option<&'a str>,
|
||||
+ pub author: Option<&'a str>, // case-insensitive match
|
||||
@@
|
||||
- // Filter by time (7d, 2w, 1m, or YYYY-MM-DD)
|
||||
+ // Filter by start time (7d, 2w, 1m, or YYYY-MM-DD)
|
||||
pub since: Option<String>,
|
||||
+ /// Filter by end time (7d, 2w, 1m, or YYYY-MM-DD)
|
||||
+ #[arg(long, help_heading = "Filters")]
|
||||
+ pub until: Option<String>,
|
||||
@@
|
||||
- /// Only show unresolved review comments
|
||||
- pub unresolved: bool,
|
||||
+ /// Resolution filter: any, unresolved, resolved
|
||||
+ #[arg(long, value_parser = ["any", "unresolved", "resolved"], default_value = "any", help_heading = "Filters")]
|
||||
+ pub resolution: String,
|
||||
```
|
||||
|
||||
5. **Broaden index strategy to match actual query shapes, not just author queries**
|
||||
Rationale: `idx_notes_user_created` helps one path, but common usage also includes project+time scans and unresolved filters. Add two more partial composites for high-selectivity paths.
|
||||
|
||||
```diff
|
||||
@@ ### Work Chunk 1E: Composite Query Index
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_user_created
|
||||
ON notes(project_id, author_username, created_at DESC, id DESC)
|
||||
WHERE is_system = 0;
|
||||
+
|
||||
+CREATE INDEX IF NOT EXISTS idx_notes_project_created
|
||||
+ON notes(project_id, created_at DESC, id DESC)
|
||||
+WHERE is_system = 0;
|
||||
+
|
||||
+CREATE INDEX IF NOT EXISTS idx_notes_unresolved_project_created
|
||||
+ON notes(project_id, created_at DESC, id DESC)
|
||||
+WHERE is_system = 0 AND resolvable = 1 AND resolved = 0;
|
||||
@@
|
||||
+#[test]
|
||||
+fn test_notes_query_plan_uses_project_created_index_for_default_listing() { ... }
|
||||
+#[test]
|
||||
+fn test_notes_query_plan_uses_unresolved_index_when_resolution_unresolved() { ... }
|
||||
```
|
||||
|
||||
6. **Improve per-note document payload with structured metadata header + minimal thread context**
|
||||
Rationale: isolated single-note docs can lose meaning. A small structured header plus lightweight context (parent + one preceding note excerpt) improves semantic retrieval quality substantially without re-bundling full threads.
|
||||
|
||||
```diff
|
||||
@@ ### Work Chunk 2C: Note Document Extractor
|
||||
-// 6. Format content:
|
||||
-// [[Note]] {note_type or "Comment"} on {parent_type_prefix}: {parent_title}
|
||||
-// Project: {path_with_namespace}
|
||||
-// URL: {url}
|
||||
-// Author: @{author}
|
||||
-// Date: {format_date(created_at)}
|
||||
-// Labels: {labels_json}
|
||||
-// File: {position_new_path}:{position_new_line} (if DiffNote)
|
||||
-//
|
||||
-// --- Body ---
|
||||
-//
|
||||
-// {body}
|
||||
+// 6. Format content with machine-readable header:
|
||||
+// [[Note]]
|
||||
+// source_type: note
|
||||
+// note_gitlab_id: {gitlab_id}
|
||||
+// project: {path_with_namespace}
|
||||
+// parent_type: {Issue|MergeRequest}
|
||||
+// parent_iid: {iid}
|
||||
+// note_type: {DiffNote|DiscussionNote|Comment}
|
||||
+// author: @{author}
|
||||
+// created_at: {iso8601}
|
||||
+// resolved: {true|false}
|
||||
+// path: {position_new_path}:{position_new_line}
|
||||
+// url: {url}
|
||||
+//
|
||||
+// --- Context ---
|
||||
+// parent_title: {title}
|
||||
+// previous_note_excerpt: {optional, max 200 chars}
|
||||
+//
|
||||
+// --- Body ---
|
||||
+// {body}
|
||||
```
|
||||
|
||||
7. **Add first-class export modes for downstream profiling pipelines**
|
||||
Rationale: this makes the feature much more useful immediately (LLM prompts, notebook analysis, external scripts) without adding a profiling command. It stays within your non-goals and increases adoption.
|
||||
|
||||
```diff
|
||||
@@ pub struct NotesArgs {
|
||||
+ /// Output format
|
||||
+ #[arg(long, value_parser = ["table", "json", "jsonl", "csv"], default_value = "table", help_heading = "Output")]
|
||||
+ pub format: String,
|
||||
@@
|
||||
- if robot_mode {
|
||||
+ if robot_mode || args.format == "json" || args.format == "jsonl" || args.format == "csv" {
|
||||
print_list_notes_json(...)
|
||||
} else {
|
||||
print_list_notes(&result);
|
||||
}
|
||||
@@ ### Work Chunk 1C: Human & Robot Output Formatting
|
||||
+Add `print_list_notes_csv()` and `print_list_notes_jsonl()`:
|
||||
+- CSV columns mirror `NoteListRowJson` field names
|
||||
+- JSONL emits one note object per line for streaming pipelines
|
||||
```
|
||||
|
||||
8. **Strengthen verification with idempotence + migration data-preservation checks**
|
||||
Rationale: this feature touches ingestion, migrations, indexing, and regeneration. Add explicit idempotence/perf checks so regressions surface early.
|
||||
|
||||
```diff
|
||||
@@ ## Verification Checklist
|
||||
cargo test
|
||||
cargo clippy --all-targets -- -D warnings
|
||||
cargo fmt --check
|
||||
+cargo test test_note_ingestion_idempotent_across_two_syncs
|
||||
+cargo test test_note_document_count_stable_after_second_generate_docs_full
|
||||
@@
|
||||
+lore sync
|
||||
+lore generate-docs --full
|
||||
+lore -J stats > /tmp/stats1.json
|
||||
+lore generate-docs --full
|
||||
+lore -J stats > /tmp/stats2.json
|
||||
+# assert note doc count unchanged and dirty queue drains to zero
|
||||
```
|
||||
|
||||
If you want, I can turn this into a fully rewritten PRD v2 draft with these changes merged in-place and renumbered work chunks end-to-end.
|
||||
162
docs/prd-per-note-search.feedback-3.md
Normal file
162
docs/prd-per-note-search.feedback-3.md
Normal file
@@ -0,0 +1,162 @@
|
||||
These are the highest-impact revisions I’d make. They avoid everything in your `## Rejected Recommendations` list.
|
||||
|
||||
1. Add immediate note-document deletion propagation (don’t wait for `generate-docs --full`)
|
||||
Why: right now, deleted notes can leave stale `source_type='note'` documents until a full rebuild. That creates incorrect search/reporting results and weakens trust in the dataset.
|
||||
```diff
|
||||
@@ Phase 0: Stable Note Identity
|
||||
+### Work Chunk 0B: Immediate Deletion Propagation
|
||||
+
|
||||
+When sweep deletes stale notes, propagate deletion to documents in the same transaction.
|
||||
+Do not rely on eventual cleanup via `generate-docs --full`.
|
||||
+
|
||||
+#### Tests to Write First
|
||||
+#[test]
|
||||
+fn test_issue_note_sweep_deletes_note_documents_immediately() { ... }
|
||||
+#[test]
|
||||
+fn test_mr_note_sweep_deletes_note_documents_immediately() { ... }
|
||||
+
|
||||
+#### Implementation
|
||||
+Use `DELETE ... RETURNING id, is_system` in note sweep functions.
|
||||
+For returned non-system note ids:
|
||||
+1) `DELETE FROM documents WHERE source_type='note' AND source_id=?`
|
||||
+2) `DELETE FROM dirty_sources WHERE source_type='note' AND source_id=?`
|
||||
```
|
||||
|
||||
2. Add one-time upgrade backfill for existing notes (migration 024)
|
||||
Why: existing DBs will otherwise only get note-documents for changed/new notes. Historical notes remain invisible unless users manually run full rebuild.
|
||||
```diff
|
||||
@@ Phase 2: Per-Note Documents
|
||||
+### Work Chunk 2H: Backfill Existing Notes After Upgrade (Migration 024)
|
||||
+
|
||||
+Create migration `024_note_dirty_backfill.sql`:
|
||||
+INSERT INTO dirty_sources (source_type, source_id, queued_at)
|
||||
+SELECT 'note', n.id, unixepoch('now') * 1000
|
||||
+FROM notes n
|
||||
+LEFT JOIN documents d
|
||||
+ ON d.source_type='note' AND d.source_id=n.id
|
||||
+WHERE n.is_system=0 AND d.id IS NULL
|
||||
+ON CONFLICT(source_type, source_id) DO NOTHING;
|
||||
+
|
||||
+Add migration test asserting idempotence and expected queue size.
|
||||
```
|
||||
|
||||
3. Fix `--since/--until` semantics and validation
|
||||
Why: reusing `parse_since` for `until` creates ambiguous windows and off-by-boundary behavior; your own example `--since 90d --until 180d` is chronologically reversed.
|
||||
```diff
|
||||
@@ Work Chunk 1A: Data Types & Query Layer
|
||||
- since: parse_since(since_str) then n.created_at >= ?
|
||||
- until: parse_since(until_str) then n.created_at <= ?
|
||||
+ since: parse_since_start_bound(since_str) then n.created_at >= ?
|
||||
+ until: parse_until_end_bound(until_str) then n.created_at <= ?
|
||||
+ Validate since <= until; otherwise return a clear user error.
|
||||
+
|
||||
+#### Tests to Write First
|
||||
+#[test] fn test_query_notes_invalid_time_window_rejected() { ... }
|
||||
+#[test] fn test_query_notes_until_date_is_end_of_day_inclusive() { ... }
|
||||
```
|
||||
|
||||
4. Separate semantic-change detection from housekeeping updates
|
||||
Why: current proposed `WHERE` includes `updated_at`, which will cause unnecessary dirty churn. You want `last_seen_at` to always refresh, but regeneration only when searchable semantics changed.
|
||||
```diff
|
||||
@@ Work Chunk 0A: Upsert/Sweep for Issue Discussion Notes
|
||||
- OR notes.updated_at IS NOT excluded.updated_at
|
||||
+ -- updated_at-only changes should not mark semantic dirty
|
||||
+
|
||||
+Perform two-step logic:
|
||||
+1) Upsert always updates persistence/housekeeping fields (`updated_at`, `last_seen_at`).
|
||||
+2) `changed_semantics` is computed only from fields used by note documents/search filters
|
||||
+ (body, note_type, resolved flags, paths, author, parent linkage).
|
||||
+
|
||||
+#### Tests to Write First
|
||||
+#[test]
|
||||
+fn test_issue_note_upsert_updated_at_only_does_not_mark_semantic_change() { ... }
|
||||
```
|
||||
|
||||
5. Make indexes align with actual query collation and join strategy
|
||||
Why: `author` uses `COLLATE NOCASE`; without collation-aware index, SQLite can skip index use. Also, IID filters via scalar subqueries are harder for planner than direct join predicates.
|
||||
```diff
|
||||
@@ Work Chunk 1E: Composite Query Index
|
||||
-CREATE INDEX ... ON notes(project_id, author_username, created_at DESC, id DESC) WHERE is_system = 0;
|
||||
+CREATE INDEX ... ON notes(project_id, author_username COLLATE NOCASE, created_at DESC, id DESC) WHERE is_system = 0;
|
||||
+
|
||||
+CREATE INDEX IF NOT EXISTS idx_discussions_issue_id ON discussions(issue_id);
|
||||
+CREATE INDEX IF NOT EXISTS idx_discussions_mr_id ON discussions(merge_request_id);
|
||||
```
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 1A: query_notes()
|
||||
- d.issue_id = (SELECT id FROM issues WHERE iid = ? AND project_id = ?)
|
||||
+ i.iid = ? AND i.project_id = ?
|
||||
- d.merge_request_id = (SELECT id FROM merge_requests WHERE iid = ? AND project_id = ?)
|
||||
+ m.iid = ? AND m.project_id = ?
|
||||
```
|
||||
|
||||
6. Replace manual CSV escaping with `csv` crate
|
||||
Why: manual RFC4180 escaping is fragile (quotes/newlines/multi-byte edge cases). This is exactly where a mature library reduces long-term bug risk.
|
||||
```diff
|
||||
@@ Work Chunk 1C: Human & Robot Output Formatting
|
||||
- Uses a minimal CSV writer (no external dependency — the format is simple enough for manual escaping).
|
||||
+ Uses `csv::Writer` for RFC4180-compliant escaping and stable output across edge cases.
|
||||
+
|
||||
+#### Tests to Write First
|
||||
+#[test] fn test_csv_output_multiline_and_quotes_roundtrip() { ... }
|
||||
```
|
||||
|
||||
7. Add `--contains` lexical body filter to `lore notes`
|
||||
Why: useful middle ground between metadata filtering and semantic search; great for reviewer-pattern mining without requiring FTS query syntax.
|
||||
```diff
|
||||
@@ Work Chunk 1B: CLI Arguments & Command Wiring
|
||||
+/// Filter by case-insensitive substring in note body
|
||||
+#[arg(long, help_heading = "Filters")]
|
||||
+pub contains: Option<String>;
|
||||
```
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 1A: NoteListFilters
|
||||
+ pub contains: Option<&'a str>,
|
||||
@@ query_notes dynamic filters
|
||||
+ if contains.is_some() {
|
||||
+ where_clauses.push("n.body LIKE ? COLLATE NOCASE");
|
||||
+ params.push(format!("%{}%", escape_like(contains.unwrap())));
|
||||
+ }
|
||||
```
|
||||
|
||||
8. Reduce note-document embedding noise by slimming metadata header
|
||||
Why: current verbose key-value header repeats low-signal tokens and consumes embedding budget. Keep context, but bias tokens toward actual review text.
|
||||
```diff
|
||||
@@ Work Chunk 2C: Note Document Extractor
|
||||
- Build content with structured metadata header:
|
||||
- [[Note]]
|
||||
- source_type: note
|
||||
- note_gitlab_id: ...
|
||||
- project: ...
|
||||
- ...
|
||||
- --- Body ---
|
||||
- {body}
|
||||
+ Build content with compact, high-signal layout:
|
||||
+ [[Note]]
|
||||
+ @{author} on {Issue#|MR!}{iid} in {project_path}
|
||||
+ path: {path:line} (only when available)
|
||||
+ state: {resolved|unresolved} (only when resolvable)
|
||||
+
|
||||
+ {body}
|
||||
+
|
||||
+Keep detailed metadata in structured document columns/labels/paths/url,
|
||||
+not repeated in verbose text.
|
||||
```
|
||||
|
||||
9. Add explicit performance regression checks for the new hot paths
|
||||
Why: this feature increases document volume ~4x; you should pin acceptable query behavior now so future changes don’t silently degrade.
|
||||
```diff
|
||||
@@ Verification Checklist
|
||||
+Performance/plan checks:
|
||||
+1) `EXPLAIN QUERY PLAN` for:
|
||||
+ - author+since query
|
||||
+ - project+date query
|
||||
+ - for-mr / for-issue query
|
||||
+2) Seed 50k-note synthetic fixture and assert:
|
||||
+ - `lore notes --author ... --limit 100` stays under agreed local threshold
|
||||
+ - `lore search --type note ...` remains deterministic and completes successfully
|
||||
```
|
||||
|
||||
If you want, I can also provide a fully merged “iteration 3” PRD text with these edits applied end-to-end so you can drop it in directly.
|
||||
187
docs/prd-per-note-search.feedback-4.md
Normal file
187
docs/prd-per-note-search.feedback-4.md
Normal file
@@ -0,0 +1,187 @@
|
||||
1. **Canonical note identity for documents: use `notes.gitlab_id` as `source_id`**
|
||||
Why this is better: the current plan still couples document identity to local row IDs. Even with upsert+sweep, local IDs are a storage artifact and can be reused in edge cases. Using GitLab note IDs as canonical document IDs makes regeneration, backfill, and deletion propagation more stable and portable.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ Phase 0: Stable Note Identity
|
||||
-Phase 2 depends on `notes.id` as the `source_id` for note documents.
|
||||
+Phase 2 uses `notes.gitlab_id` as the `source_id` for note documents.
|
||||
+`notes.id` remains an internal relational key only.
|
||||
|
||||
@@ Work Chunk 0A
|
||||
pub struct NoteUpsertOutcome {
|
||||
pub local_note_id: i64,
|
||||
+ pub document_source_id: i64, // notes.gitlab_id
|
||||
pub changed_semantics: bool,
|
||||
}
|
||||
|
||||
@@ Work Chunk 2D
|
||||
-if !note.is_system && outcome.changed_semantics {
|
||||
- dirty_tracker::mark_dirty_tx(&tx, SourceType::Note, outcome.local_note_id)?;
|
||||
+if !note.is_system && outcome.changed_semantics {
|
||||
+ dirty_tracker::mark_dirty_tx(&tx, SourceType::Note, outcome.document_source_id)?;
|
||||
}
|
||||
|
||||
@@ Work Chunk 2E
|
||||
-SELECT 'note', n.id, ?1
|
||||
+SELECT 'note', n.gitlab_id, ?1
|
||||
|
||||
@@ Work Chunk 2H
|
||||
-ON d.source_type = 'note' AND d.source_id = n.id
|
||||
+ON d.source_type = 'note' AND d.source_id = n.gitlab_id
|
||||
```
|
||||
|
||||
2. **Prevent false deletions on partial/incomplete syncs**
|
||||
Why this is better: sweep-based deletion is correct only when a discussion’s notes were fully fetched. If a page fails mid-fetch, current logic can incorrectly delete valid notes. Add an explicit “fetch complete” guard before sweep.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ Phase 0
|
||||
+### Work Chunk 0C: Sweep Safety Guard (Partial Fetch Protection)
|
||||
+
|
||||
+Only run stale-note sweep when note pagination completed successfully for that discussion.
|
||||
+If fetch is partial/interrupted, skip sweep and keep prior notes intact.
|
||||
|
||||
+#### Tests to Write First
|
||||
+#[test]
|
||||
+fn test_partial_fetch_does_not_sweep_notes() { /* ... */ }
|
||||
+
|
||||
+#[test]
|
||||
+fn test_complete_fetch_runs_sweep_notes() { /* ... */ }
|
||||
|
||||
+#### Implementation
|
||||
+if discussion_fetch_complete {
|
||||
+ sweep_stale_issue_notes(...)?;
|
||||
+} else {
|
||||
+ tracing::warn!("Skipping stale sweep for discussion {} due to partial fetch", discussion_gitlab_id);
|
||||
+}
|
||||
```
|
||||
|
||||
3. **Make deletion propagation set-based (not per-note loop)**
|
||||
Why this is better: the current per-note DELETE loop is O(N) statements and gets slow on large threads. A temp-table/CTE set-based delete is faster, simpler to reason about, and remains atomic.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ Work Chunk 0B Implementation
|
||||
- for note_id in stale_note_ids {
|
||||
- conn.execute("DELETE FROM documents WHERE source_type = 'note' AND source_id = ?", [note_id])?;
|
||||
- conn.execute("DELETE FROM dirty_sources WHERE source_type = 'note' AND source_id = ?", [note_id])?;
|
||||
- }
|
||||
+ CREATE TEMP TABLE _stale_note_source_ids(source_id INTEGER PRIMARY KEY) WITHOUT ROWID;
|
||||
+ INSERT INTO _stale_note_source_ids
|
||||
+ SELECT gitlab_id
|
||||
+ FROM notes
|
||||
+ WHERE discussion_id = ? AND last_seen_at < ? AND is_system = 0;
|
||||
+
|
||||
+ DELETE FROM notes
|
||||
+ WHERE discussion_id = ? AND last_seen_at < ?;
|
||||
+
|
||||
+ DELETE FROM documents
|
||||
+ WHERE source_type = 'note'
|
||||
+ AND source_id IN (SELECT source_id FROM _stale_note_source_ids);
|
||||
+
|
||||
+ DELETE FROM dirty_sources
|
||||
+ WHERE source_type = 'note'
|
||||
+ AND source_id IN (SELECT source_id FROM _stale_note_source_ids);
|
||||
+
|
||||
+ DROP TABLE _stale_note_source_ids;
|
||||
```
|
||||
|
||||
4. **Fix project-scoping and time-window semantics in `lore notes`**
|
||||
Why this is better: the plan currently has a contradiction: clap `requires = "project"` blocks use of `defaultProject`, while query layer says default fallback is allowed. Also, `since/until` parsing should use one shared “now” to avoid subtle drift and inverted windows.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ Work Chunk 1B NotesArgs
|
||||
-#[arg(long = "for-issue", ..., requires = "project")]
|
||||
+#[arg(long = "for-issue", ...)]
|
||||
pub for_issue: Option<i64>;
|
||||
|
||||
-#[arg(long = "for-mr", ..., requires = "project")]
|
||||
+#[arg(long = "for-mr", ...)]
|
||||
pub for_mr: Option<i64>;
|
||||
|
||||
@@ Work Chunk 1A Query Notes
|
||||
-- `since`: `parse_since(since_str)` then `n.created_at >= ?`
|
||||
-- `until`: `parse_since(until_str)` then `n.created_at <= ?`
|
||||
+- Parse `since` and `until` with a single anchored `now_ms` captured once per command.
|
||||
+- If user supplies `YYYY-MM-DD` for `--until`, interpret as end-of-day (23:59:59.999 UTC).
|
||||
+- Validate `since <= until` after both parse with same anchor.
|
||||
```
|
||||
|
||||
5. **Add an analytics mode (not a profile command): `lore notes --aggregate`**
|
||||
Why this is better: this directly supports the stated use case (review patterns) without introducing the rejected “profile report” command. It keeps scope narrow and reuses existing filters.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ Phase 1
|
||||
+### Work Chunk 1F: Aggregation Mode for Notes Listing
|
||||
+
|
||||
+Add optional aggregation on top of `lore notes`:
|
||||
+- `--aggregate author|note_type|path|resolution`
|
||||
+- `--top N` (default 20)
|
||||
+
|
||||
+Behavior:
|
||||
+- Reuses all existing filters (`--since`, `--project`, `--for-mr`, etc.)
|
||||
+- Returns grouped counts (+ percentage of filtered corpus)
|
||||
+- Works in table/json/jsonl/csv
|
||||
+
|
||||
+Non-goal alignment:
|
||||
+- This is not a narrative “reviewer profile” command.
|
||||
+- It is a query primitive for downstream analysis.
|
||||
```
|
||||
|
||||
6. **Prevent note backfill from starving other document regeneration**
|
||||
Why this is better: after migration/backfill, note dirty entries can dominate the queue and delay issue/MR/discussion updates. Add source-type fairness in regenerator scheduling.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ Work Chunk 2D
|
||||
+#### Scheduling Revision
|
||||
+Process dirty sources with weighted fairness instead of strict FIFO:
|
||||
+- issue: 3
|
||||
+- merge_request: 3
|
||||
+- discussion: 2
|
||||
+- note: 1
|
||||
+
|
||||
+Implementation sketch:
|
||||
+- fetch next batch by source_type buckets
|
||||
+- interleave according to weights
|
||||
+- preserve retry semantics per source
|
||||
|
||||
+#### Tests to Write First
|
||||
+#[test]
|
||||
+fn test_note_backfill_does_not_starve_issue_and_mr_regeneration() { /* ... */ }
|
||||
```
|
||||
|
||||
7. **Harden migration 023: remove invalid SQL assertions and move integrity checks to tests**
|
||||
Why this is better: `RAISE(ABORT, ...)` in standalone `SELECT` is not valid SQLite usage outside triggers/check expressions. Keep migration SQL minimal/portable and enforce invariants in migration tests.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ Work Chunk 2A Migration SQL
|
||||
--- Step 10: Integrity verification
|
||||
-SELECT CASE
|
||||
- WHEN ... THEN RAISE(ABORT, '...')
|
||||
-END;
|
||||
+-- Step 10 removed from SQL migration.
|
||||
+-- Integrity verification is enforced in migration tests:
|
||||
+-- 1) pre/post row-count equality
|
||||
+-- 2) `PRAGMA foreign_key_check` is empty
|
||||
+-- 3) documents_fts row count matches documents row count after rebuild
|
||||
|
||||
@@ Work Chunk 2A Tests
|
||||
+#[test]
|
||||
+fn test_migration_023_integrity_checks_pass() {
|
||||
+ // pre/post counts, foreign_key_check empty, fts parity
|
||||
+}
|
||||
```
|
||||
|
||||
These 7 revisions improve correctness under failure, reduce churn risk, improve large-sync performance, and make the feature materially more useful for reviewer-analysis workflows without reintroducing any rejected recommendations.
|
||||
190
docs/prd-per-note-search.feedback-5.md
Normal file
190
docs/prd-per-note-search.feedback-5.md
Normal file
@@ -0,0 +1,190 @@
|
||||
Here are the highest-impact revisions I’d make. None of these repeat anything in your `## Rejected Recommendations`.
|
||||
|
||||
1. **Add immutable reviewer identity (`author_id`) as a first-class key**
|
||||
Why this improves the plan: the PRD’s core use case is year-scale reviewer profiling. Usernames are mutable in GitLab, so username-only filtering will fragment one reviewer into multiple identities over time. Adding `author_id` closes that correctness hole and makes historical analysis reliable.
|
||||
|
||||
```diff
|
||||
@@ Problem Statement
|
||||
-1. **Query individual notes by author** — the `--author` filter on `lore search` only matches the first note's author per discussion thread
|
||||
+1. **Query individual notes by reviewer identity** — support both mutable username and immutable GitLab `author_id` for stable longitudinal analysis
|
||||
|
||||
@@ Phase 0: Stable Note Identity
|
||||
+### Work Chunk 0D: Immutable Author Identity Capture
|
||||
+**Files:** `migrations/025_notes_author_id.sql`, `src/ingestion/discussions.rs`, `src/ingestion/mr_discussions.rs`, `src/cli/commands/list.rs`
|
||||
+
|
||||
+#### Implementation
|
||||
+- Add nullable `notes.author_id INTEGER` and backfill from future syncs.
|
||||
+- Populate `author_id` from GitLab note payload (`note.author.id`) on both issue and MR note ingestion paths.
|
||||
+- Add `--author-id <int>` filter to `lore notes`.
|
||||
+- Keep `--author` for ergonomics; when both provided, require both to match.
|
||||
+
|
||||
+#### Indexing
|
||||
+- Add `idx_notes_author_id_created ON notes(project_id, author_id, created_at DESC, id DESC) WHERE is_system = 0;`
|
||||
+
|
||||
+#### Tests
|
||||
+- `test_query_notes_filter_author_id_survives_username_change`
|
||||
+- `test_query_notes_author_and_author_id_intersection`
|
||||
```
|
||||
|
||||
2. **Strengthen partial-fetch safety from a boolean to an explicit fetch state contract**
|
||||
Why this improves the plan: `fetch_complete: bool` is easy to misuse and fragile under retries/crashes. A run-scoped state model makes sweep correctness auditable and prevents accidental deletions when ingestion aborts midway.
|
||||
|
||||
```diff
|
||||
@@ Phase 0: Stable Note Identity
|
||||
-### Work Chunk 0C: Sweep Safety Guard (Partial Fetch Protection)
|
||||
+### Work Chunk 0C: Sweep Safety Guard with Run-Scoped Fetch State
|
||||
|
||||
@@ Implementation
|
||||
-Add a `fetch_complete` parameter to the discussion ingestion functions. Only run the stale-note sweep when the fetch completed successfully:
|
||||
+Add a run-scoped fetch state:
|
||||
+- `FetchState::Complete`
|
||||
+- `FetchState::Partial`
|
||||
+- `FetchState::Failed`
|
||||
+
|
||||
+Only run sweep on `FetchState::Complete`.
|
||||
+Persist `run_seen_at` once per sync run and pass unchanged through all discussion/note upserts.
|
||||
+Require `run_seen_at` monotonicity per discussion before sweep (skip and warn otherwise).
|
||||
|
||||
@@ Tests to Write First
|
||||
+#[test]
|
||||
+fn test_failed_fetch_never_sweeps_even_after_partial_upserts() { ... }
|
||||
+#[test]
|
||||
+fn test_non_monotonic_run_seen_at_skips_sweep() { ... }
|
||||
+#[test]
|
||||
+fn test_retry_after_failed_fetch_then_complete_sweeps_correctly() { ... }
|
||||
```
|
||||
|
||||
3. **Add DB-level cleanup triggers for note-document referential integrity**
|
||||
Why this improves the plan: Work Chunk 0B handles the sweep path, but not every possible delete path. DB triggers give defense-in-depth so stale note docs cannot survive even if a future code path deletes notes differently.
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 0B: Immediate Deletion Propagation
|
||||
-Update both sweep functions to propagate deletion to documents and dirty_sources using set-based SQL
|
||||
+Keep set-based SQL in sweep functions, and add DB-level cleanup triggers as a safety net.
|
||||
|
||||
@@ Work Chunk 2A: Schema Migration (023)
|
||||
+-- Cleanup trigger: deleting a non-system note must delete note document + dirty queue row
|
||||
+CREATE TRIGGER notes_ad_cleanup AFTER DELETE ON notes
|
||||
+WHEN old.is_system = 0
|
||||
+BEGIN
|
||||
+ DELETE FROM documents
|
||||
+ WHERE source_type = 'note' AND source_id = old.id;
|
||||
+ DELETE FROM dirty_sources
|
||||
+ WHERE source_type = 'note' AND source_id = old.id;
|
||||
+END;
|
||||
+
|
||||
+-- Cleanup trigger: if note flips to system, remove its document artifacts
|
||||
+CREATE TRIGGER notes_au_system_cleanup AFTER UPDATE OF is_system ON notes
|
||||
+WHEN old.is_system = 0 AND new.is_system = 1
|
||||
+BEGIN
|
||||
+ DELETE FROM documents
|
||||
+ WHERE source_type = 'note' AND source_id = new.id;
|
||||
+ DELETE FROM dirty_sources
|
||||
+ WHERE source_type = 'note' AND source_id = new.id;
|
||||
+END;
|
||||
```
|
||||
|
||||
4. **Eliminate N+1 extraction cost with parent metadata caching in regeneration**
|
||||
Why this improves the plan: backfilling ~8k notes with per-note parent/label lookups creates avoidable query amplification. Batch caching turns repeated joins into one-time lookups per parent entity and materially reduces rebuild time.
|
||||
|
||||
```diff
|
||||
@@ Phase 2: Per-Note Documents
|
||||
+### Work Chunk 2I: Batch Parent Metadata Cache for Note Regeneration
|
||||
+**Files:** `src/documents/regenerator.rs`, `src/documents/extractor.rs`
|
||||
+
|
||||
+#### Implementation
|
||||
+- Add `NoteExtractionContext` cache keyed by `(noteable_type, parent_id)` containing:
|
||||
+ - parent iid/title/url
|
||||
+ - parent labels
|
||||
+ - project path
|
||||
+- In batch regeneration, prefetch parent metadata for note IDs in the current chunk.
|
||||
+- Use cached metadata in `extract_note_document()` to avoid repeated parent/label queries.
|
||||
+
|
||||
+#### Tests
|
||||
+- `test_note_regeneration_uses_parent_cache_consistently`
|
||||
+- `test_note_regeneration_cache_hit_preserves_hash_determinism`
|
||||
```
|
||||
|
||||
5. **Add embedding dedup cache keyed by semantic text hash**
|
||||
Why this improves the plan: note docs will contain repeated short comments (“LGTM”, “nit: …”). Current doc-level hashing includes metadata, so identical semantic comments still re-embed many times. A semantic embedding hash cache cuts cost and speeds full rebuild/backfill without changing search behavior.
|
||||
|
||||
```diff
|
||||
@@ Phase 2: Per-Note Documents
|
||||
+### Work Chunk 2J: Semantic Embedding Dedup for Notes
|
||||
+**Files:** `migrations/026_embedding_cache.sql`, embedding pipeline module(s), `src/documents/extractor.rs`
|
||||
+
|
||||
+#### Implementation
|
||||
+- Compute `embedding_text` for notes as: normalized note body + compact stable context (`parent_type`, `path`, `resolution`), excluding volatile fields.
|
||||
+- Compute `embedding_hash = sha256(embedding_text)`.
|
||||
+- Before embedding generation, lookup existing vector by `(model, embedding_hash)`.
|
||||
+- Reuse cached vector when present; only call embedding model on misses.
|
||||
+
|
||||
+#### Tests
|
||||
+- `test_identical_note_bodies_reuse_embedding_vector`
|
||||
+- `test_embedding_hash_changes_when_semantic_context_changes`
|
||||
```
|
||||
|
||||
6. **Add deterministic review-signal tags as derived labels**
|
||||
Why this improves the plan: this makes output immediately more useful for reviewer-pattern analysis without adding a profile command (which is explicitly out of scope). It increases practical value of both `lore notes` and `lore search --type note` with low complexity.
|
||||
|
||||
```diff
|
||||
@@ Non-Goals
|
||||
-- Adding a "reviewer profile" report command (that's a downstream use case built on this infrastructure)
|
||||
+- Adding a "reviewer profile" report command (downstream), while allowing low-level derived signal tags as indexing primitives
|
||||
|
||||
@@ Phase 2: Per-Note Documents
|
||||
+### Work Chunk 2K: Derived Review Signal Labels
|
||||
+**Files:** `src/documents/extractor.rs`
|
||||
+
|
||||
+#### Implementation
|
||||
+- Derive deterministic labels from note text + metadata:
|
||||
+ - `signal:nit`
|
||||
+ - `signal:blocking`
|
||||
+ - `signal:security`
|
||||
+ - `signal:performance`
|
||||
+ - `signal:testing`
|
||||
+- Attach via existing `document_labels` flow for note documents.
|
||||
+- No new CLI mode required; existing label filters can consume these labels.
|
||||
+
|
||||
+#### Tests
|
||||
+- `test_note_document_derives_signal_labels_nit`
|
||||
+- `test_note_document_derives_signal_labels_security`
|
||||
+- `test_signal_label_derivation_is_deterministic`
|
||||
```
|
||||
|
||||
7. **Add high-precision note targeting filters (`--note-id`, `--gitlab-note-id`, `--discussion-id`)**
|
||||
Why this improves the plan: debugging, incident response, and reproducibility all benefit from exact addressing. This is especially useful when validating sync correctness and cross-checking a specific note/document lifecycle.
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 1B: CLI Arguments & Command Wiring
|
||||
pub struct NotesArgs {
|
||||
+ /// Filter by local note row id
|
||||
+ #[arg(long = "note-id", help_heading = "Filters")]
|
||||
+ pub note_id: Option<i64>,
|
||||
+
|
||||
+ /// Filter by GitLab note id
|
||||
+ #[arg(long = "gitlab-note-id", help_heading = "Filters")]
|
||||
+ pub gitlab_note_id: Option<i64>,
|
||||
+
|
||||
+ /// Filter by local discussion id
|
||||
+ #[arg(long = "discussion-id", help_heading = "Filters")]
|
||||
+ pub discussion_id: Option<i64>,
|
||||
}
|
||||
|
||||
@@ Work Chunk 1A: Filter struct
|
||||
pub struct NoteListFilters<'a> {
|
||||
+ pub note_id: Option<i64>,
|
||||
+ pub gitlab_note_id: Option<i64>,
|
||||
+ pub discussion_id: Option<i64>,
|
||||
}
|
||||
|
||||
@@ Tests to Write First
|
||||
+#[test]
|
||||
+fn test_query_notes_filter_note_id_exact() { ... }
|
||||
+#[test]
|
||||
+fn test_query_notes_filter_gitlab_note_id_exact() { ... }
|
||||
+#[test]
|
||||
+fn test_query_notes_filter_discussion_id_exact() { ... }
|
||||
```
|
||||
|
||||
If you want, I can produce a single consolidated “iteration 5” PRD diff that merges these into your exact section ordering and updates the dependency graph/migration numbering end-to-end.
|
||||
131
docs/prd-per-note-search.feedback-6.md
Normal file
131
docs/prd-per-note-search.feedback-6.md
Normal file
@@ -0,0 +1,131 @@
|
||||
1. **Make immutable identity usable now (`--author-id`)**
|
||||
Why: The plan captures `author_id` but intentionally defers using it, so the core longitudinal-analysis problem is only half-fixed.
|
||||
|
||||
```diff
|
||||
@@ Phase 1: `lore notes` Command / Work Chunk 1A
|
||||
pub struct NoteListFilters<'a> {
|
||||
+ pub author_id: Option<i64>, // immutable identity filter
|
||||
@@
|
||||
- pub author: Option<&'a str>, // case-insensitive match via COLLATE NOCASE
|
||||
+ pub author: Option<&'a str>, // display-name filter
|
||||
+ // If both author and author_id are provided, apply both (AND) for precision.
|
||||
}
|
||||
@@
|
||||
Filter mappings:
|
||||
+ - `author_id`: `n.author_id = ?` (exact immutable identity)
|
||||
- `author`: strip `@` prefix, `n.author_username = ? COLLATE NOCASE`
|
||||
@@ Phase 1 / Work Chunk 1B (CLI)
|
||||
+ /// Filter by immutable author id
|
||||
+ #[arg(long = "author-id", help_heading = "Filters")]
|
||||
+ pub author_id: Option<i64>,
|
||||
@@ Phase 2 / Work Chunk 2F
|
||||
+ Add `--author-id` support to `lore search` filtering for note documents.
|
||||
@@ Phase 1 / Work Chunk 1E
|
||||
+ CREATE INDEX IF NOT EXISTS idx_notes_project_author_id_created
|
||||
+ ON notes(project_id, author_id, created_at DESC, id DESC)
|
||||
+ WHERE is_system = 0 AND author_id IS NOT NULL;
|
||||
```
|
||||
|
||||
2. **Fix document staleness on username changes**
|
||||
Why: Current plan says username changes are “not semantic,” but note documents include username in content/title, so docs go stale/inconsistent.
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 0D: Immutable Author Identity Capture
|
||||
- Assert: changed_semantics = false (username change is not a semantic change for documents)
|
||||
+ Assert: changed_semantics = true (username affects note document content/title)
|
||||
@@ Work Chunk 0A: semantic-change detection
|
||||
- old_body != body || old_note_type != note_type || ...
|
||||
+ old_body != body || old_note_type != note_type || ...
|
||||
+ || old_author_username != author_username
|
||||
@@ Work Chunk 2C: Note Document Extractor header
|
||||
author: @{author}
|
||||
+ author_id: {author_id}
|
||||
```
|
||||
|
||||
3. **Replace `last_seen_at` sweep marker with monotonic `sync_run_id`**
|
||||
Why: Timestamp markers are vulnerable to clock skew and concurrent runs; run IDs are deterministic and safer.
|
||||
|
||||
```diff
|
||||
@@ Phase 0: Stable Note Identity
|
||||
+ ### Work Chunk 0E: Monotonic Run Marker
|
||||
+ Add `sync_runs` table and `notes.last_seen_run_id`.
|
||||
+ Ingest assigns one run_id per sync transaction.
|
||||
+ Upsert sets `last_seen_run_id = current_run_id`.
|
||||
+ Sweep condition becomes `last_seen_run_id < current_run_id` (when fetch_complete=true).
|
||||
@@ Work Chunk 0C
|
||||
- fetch_complete + last_seen_at-based sweep
|
||||
+ fetch_complete + run_id-based sweep
|
||||
```
|
||||
|
||||
4. **Materialize stale-note set once during sweep**
|
||||
Why: Current set-based SQL still re-runs the stale subquery 3 times; materializing once improves performance and guarantees identical deletion set.
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 0B: Immediate Deletion Propagation
|
||||
- DELETE FROM documents ... IN (SELECT id FROM notes WHERE ...);
|
||||
- DELETE FROM dirty_sources ... IN (SELECT id FROM notes WHERE ...);
|
||||
- DELETE FROM notes WHERE ...;
|
||||
+ CREATE TEMP TABLE _stale_note_ids AS
|
||||
+ SELECT id, is_system FROM notes WHERE discussion_id = ? AND last_seen_run_id < ?;
|
||||
+ DELETE FROM documents
|
||||
+ WHERE source_type='note' AND source_id IN (SELECT id FROM _stale_note_ids WHERE is_system=0);
|
||||
+ DELETE FROM dirty_sources
|
||||
+ WHERE source_type='note' AND source_id IN (SELECT id FROM _stale_note_ids WHERE is_system=0);
|
||||
+ DELETE FROM notes WHERE id IN (SELECT id FROM _stale_note_ids);
|
||||
+ DROP TABLE _stale_note_ids;
|
||||
```
|
||||
|
||||
5. **Move historical note backfill out of migration into resumable runtime job**
|
||||
Why: Data-heavy migration can block startup and is harder to resume/recover on large DBs.
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 2H
|
||||
- Backfill Existing Notes After Upgrade (Migration 024)
|
||||
+ Backfill Existing Notes After Upgrade (Resumable Runtime Backfill)
|
||||
@@
|
||||
- Files: `migrations/024_note_dirty_backfill.sql`, `src/core/db.rs`
|
||||
+ Files: `src/documents/backfill.rs`, `src/cli/commands/generate_docs.rs`
|
||||
@@
|
||||
- INSERT INTO dirty_sources ... SELECT ... FROM notes ...
|
||||
+ Introduce batched backfill API:
|
||||
+ `enqueue_missing_note_documents(batch_size: usize) -> BackfillProgress`
|
||||
+ invoked from `generate-docs`/`sync` until complete, resumable across runs.
|
||||
```
|
||||
|
||||
6. **Add streaming path for large `jsonl`/`csv` note exports**
|
||||
Why: Current `query_notes` materializes full result set in memory; streaming improves scalability and latency.
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 1A
|
||||
+ Add `query_notes_stream(conn, filters, row_handler)` for forward-only row iteration.
|
||||
@@ Work Chunk 1C
|
||||
- print_list_notes_jsonl(&result)
|
||||
- print_list_notes_csv(&result)
|
||||
+ print_list_notes_jsonl_stream(config, filters)
|
||||
+ print_list_notes_csv_stream(config, filters)
|
||||
+ (table/json keep counted buffered path)
|
||||
```
|
||||
|
||||
7. **Add index for path-centric note queries**
|
||||
Why: `--path` + project/date queries are a stated hot path and not fully covered by current proposed indexes.
|
||||
|
||||
```diff
|
||||
@@ Work Chunk 1E: Composite Query Index
|
||||
+ CREATE INDEX IF NOT EXISTS idx_notes_project_path_created
|
||||
+ ON notes(project_id, position_new_path, created_at DESC, id DESC)
|
||||
+ WHERE is_system = 0 AND position_new_path IS NOT NULL;
|
||||
```
|
||||
|
||||
8. **Add property/invariant tests (not only examples)**
|
||||
Why: This feature touches ingestion identity, sweeping, deletion propagation, and document regeneration; randomized invariants will catch subtle regressions.
|
||||
|
||||
```diff
|
||||
@@ Verification Checklist
|
||||
+ Add property tests (proptest):
|
||||
+ - stable local IDs across randomized re-sync orderings
|
||||
+ - no orphan `documents(source_type='note')` after randomized deletions/sweeps
|
||||
+ - partial-fetch runs never reduce note count
|
||||
+ - repeated full rebuild converges (fixed-point idempotence)
|
||||
```
|
||||
|
||||
These revisions keep your existing direction, avoid all rejected items, and materially improve correctness, scale behavior, and long-term maintainability.
|
||||
2518
docs/prd-per-note-search.md
Normal file
2518
docs/prd-per-note-search.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -125,7 +125,7 @@ lore -J mrs --fields iid,title,state,draft,target_branch
|
||||
|
||||
### Available Fields
|
||||
|
||||
**Issues**: `iid`, `title`, `state`, `author_username`, `labels`, `assignees`, `discussion_count`, `unresolved_count`, `created_at_iso`, `updated_at_iso`, `web_url`, `project_path`
|
||||
**Issues**: `iid`, `title`, `state`, `author_username`, `labels`, `assignees`, `discussion_count`, `unresolved_count`, `created_at_iso`, `updated_at_iso`, `web_url`, `project_path`, `status_name`, `status_category`, `status_color`, `status_icon_name`, `status_synced_at_iso`
|
||||
|
||||
**MRs**: `iid`, `title`, `state`, `author_username`, `labels`, `draft`, `target_branch`, `source_branch`, `discussion_count`, `unresolved_count`, `created_at_iso`, `updated_at_iso`, `web_url`, `project_path`, `reviewers`
|
||||
|
||||
|
||||
541
docs/user-journeys.md
Normal file
541
docs/user-journeys.md
Normal file
@@ -0,0 +1,541 @@
|
||||
# Lore CLI User Journeys
|
||||
|
||||
## Purpose
|
||||
|
||||
Map realistic workflows for both human users and AI agents to identify gaps in the command surface and optimization opportunities. Each journey starts with a **problem** and traces the commands needed to reach a **resolution**.
|
||||
|
||||
---
|
||||
|
||||
## Part 1: Human User Flows
|
||||
|
||||
### H1. Morning Standup Prep
|
||||
|
||||
**Problem:** "What happened since yesterday? I need to know what moved before standup."
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore sync -q # Refresh data (quiet, no noise)
|
||||
lore issues -s opened --since 1d # Issues that changed overnight
|
||||
lore mrs -s opened --since 1d # MRs that moved
|
||||
lore who @me # My current workload snapshot
|
||||
```
|
||||
|
||||
**Gap identified:** No single "activity feed" command. User runs 3 queries to get what should be one view. No `--since 1d` shorthand for "since yesterday." No `@me` alias for the authenticated user.
|
||||
|
||||
---
|
||||
|
||||
### H2. Sprint Planning: What's Ready to Pick Up?
|
||||
|
||||
**Problem:** "We're planning the next sprint. What's open, unassigned, and actionable?"
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore issues -s opened -p myproject # All open issues
|
||||
lore issues -s opened -l "ready" # Issues labeled ready
|
||||
lore issues -s opened --has-due # Issues with deadlines approaching
|
||||
lore count issues -p myproject # How many total?
|
||||
```
|
||||
|
||||
**Gap identified:** No way to filter by "unassigned" issues (missing `--no-assignee` flag). No way to sort by due date. No way to see priority/weight. Can't combine filters like "opened AND no assignee AND has due date."
|
||||
|
||||
---
|
||||
|
||||
### H3. Investigating a Production Incident
|
||||
|
||||
**Problem:** "Deploy broke prod. I need the full timeline of what changed around the deploy."
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore sync -q # Get latest
|
||||
lore timeline "deploy" --since 7d # What happened around deploys
|
||||
lore search "deploy" --type mr # MRs mentioning deploy
|
||||
lore mrs 456 # Inspect the suspicious MR
|
||||
lore who --overlap src/deploy/ # Who else touches deploy code
|
||||
```
|
||||
|
||||
**Gap identified:** Timeline is keyword-based, not event-based. Can't filter by "MRs merged in the last 24 hours" directly. No way to see which MRs were merged between two dates (release diff). Would benefit from `lore mrs -s merged --since 1d`.
|
||||
|
||||
---
|
||||
|
||||
### H4. Preparing to Review Someone's MR
|
||||
|
||||
**Problem:** "I was assigned to review MR !789. I need context before diving in."
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore mrs 789 # Read the MR description + discussions
|
||||
lore mrs 789 -o # Open in browser for the actual diff
|
||||
lore who src/features/auth/ # Who are the experts in this area?
|
||||
lore search "auth refactor" --type issue # Related issues for background
|
||||
lore timeline "authentication" # History of auth changes
|
||||
```
|
||||
|
||||
**Gap identified:** No way to see the file list touched by an MR from the CLI (data is stored in `mr_file_changes` but not surfaced). No way to link an MR back to its closing issue(s) from the MR detail view. The cross-reference data exists in `entity_references` but isn't shown in `mrs <iid>` output.
|
||||
|
||||
---
|
||||
|
||||
### H5. Onboarding to an Unfamiliar Code Area
|
||||
|
||||
**Problem:** "I'm new to the team and need to understand how the billing module works."
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore search "billing" -n 20 # What exists about billing?
|
||||
lore who src/billing/ # Who knows billing best?
|
||||
lore timeline "billing" --depth 2 # History of billing changes
|
||||
lore mrs -s merged -l billing --since 6m # Recent merged billing work
|
||||
lore issues -s opened -l billing # Outstanding billing issues
|
||||
```
|
||||
|
||||
**Gap identified:** No way to get a "module overview" in one command. The search spans issues, MRs, and discussions but doesn't summarize by category. No way to see the most-discussed or most-referenced entities (high-signal items for understanding).
|
||||
|
||||
---
|
||||
|
||||
### H6. Finding the Right Reviewer for My PR
|
||||
|
||||
**Problem:** "I'm about to submit a PR touching auth and payments. Who should review?"
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore who src/features/auth/ # Auth experts
|
||||
lore who src/features/payments/ # Payment experts
|
||||
lore who @candidate1 # Check candidate1's workload
|
||||
lore who @candidate2 # Check candidate2's workload
|
||||
```
|
||||
|
||||
**Gap identified:** No way to query multiple paths at once (`lore who src/auth/ src/payments/`). No way to find the intersection of expertise. No workload-aware recommendation ("who knows this AND has bandwidth"). Four separate commands for what should be one decision.
|
||||
|
||||
---
|
||||
|
||||
### H7. Understanding Why a Feature Was Built This Way
|
||||
|
||||
**Problem:** "This code is weird. Why was it implemented like this? What was the original discussion?"
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore search "feature-name rationale" # Search for decision context
|
||||
lore timeline "feature-name" --depth 2 # Full history with cross-refs
|
||||
lore issues 234 # Read the original issue
|
||||
lore mrs 567 # Read the implementation MR
|
||||
```
|
||||
|
||||
**Gap identified:** No way to search within a specific issue's or MR's discussion notes. The search covers documents (titles + descriptions) but per-note search isn't available yet (PRD exists). No way to navigate "issue 234 was closed by MR 567" without manually knowing both IDs.
|
||||
|
||||
---
|
||||
|
||||
### H8. Checking Team Workload Before Assigning Work
|
||||
|
||||
**Problem:** "I need to assign this urgent bug. Who has the least on their plate?"
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore who @alice # Alice's workload
|
||||
lore who @bob # Bob's workload
|
||||
lore who @carol # Carol's workload
|
||||
lore who @dave # Dave's workload
|
||||
```
|
||||
|
||||
**Gap identified:** No team-level workload view. Must query each person individually. No way to list "all assignees and their open issue counts." No concept of a team roster. Would benefit from `lore who --team` or `lore workload`.
|
||||
|
||||
---
|
||||
|
||||
### H9. Preparing Release Notes
|
||||
|
||||
**Problem:** "We're cutting a release. I need to summarize what's in this version."
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore mrs -s merged --since 2w -p myproject # MRs merged since last release
|
||||
lore issues -s closed --since 2w -p myproject # Issues closed since last release
|
||||
lore mrs -s merged -l feature --since 2w # Feature MRs specifically
|
||||
lore mrs -s merged -l bugfix --since 2w # Bugfix MRs
|
||||
```
|
||||
|
||||
**Gap identified:** No way to filter by milestone (for version-based releases). Wait -- `issues` has `-m` for milestone but `mrs` does not. No changelog generation. No "what closed between tag A and tag B." No grouping by label for release note categories.
|
||||
|
||||
---
|
||||
|
||||
### H10. Finding and Closing Stale Issues
|
||||
|
||||
**Problem:** "Our backlog is bloated. Which issues haven't been touched in months?"
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore issues -s opened --sort updated --asc -n 50 # Oldest-updated first
|
||||
# Then manually inspect each one...
|
||||
lore issues 42 # Is this still relevant?
|
||||
```
|
||||
|
||||
**Gap identified:** No `--before` or `--updated-before` filter (only `--since` exists). Can sort ascending but can't filter "not updated in 90 days." No staleness indicator. No bulk operations concept.
|
||||
|
||||
---
|
||||
|
||||
### H11. Understanding a Bug's Full History
|
||||
|
||||
**Problem:** "Bug #321 keeps getting reopened. I need to understand its entire lifecycle."
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore issues 321 # Read the issue
|
||||
lore timeline "bug-keyword" -p myproject # Try to find timeline events
|
||||
# But timeline is keyword-based, not entity-based...
|
||||
```
|
||||
|
||||
**Gap identified:** No way to get a timeline for a specific entity by IID. `lore timeline` requires a keyword query, not an entity reference. Would benefit from `lore timeline --issue 321` or `lore timeline --mr 456` to get the event history of a specific entity directly.
|
||||
|
||||
---
|
||||
|
||||
### H12. Identifying Who to Ask About Failing Tests
|
||||
|
||||
**Problem:** "CI tests are failing in `src/lib/parser.rs`. Who last touched this?"
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore who src/lib/parser.rs # Expert lookup
|
||||
lore who --overlap src/lib/parser.rs # Who else has touched it
|
||||
lore search "parser" --type mr --since 2w # Recent MRs touching parser
|
||||
```
|
||||
|
||||
**Gap identified:** Expert mode uses DiffNote analysis (code review comments), not actual file change tracking. The `mr_file_changes` table has the real data but `who` doesn't use it for attribution. Could be much more accurate with file-change-based expertise.
|
||||
|
||||
---
|
||||
|
||||
### H13. Tracking a Feature Across Multiple MRs
|
||||
|
||||
**Problem:** "The 'dark mode' feature spans 5 MRs. I need to see them all together."
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore mrs -l dark-mode # MRs with the label
|
||||
lore issues -l dark-mode # Related issues
|
||||
lore timeline "dark mode" --depth 2 # Cross-referenced events
|
||||
```
|
||||
|
||||
**Gap identified:** Works reasonably well with labels as the grouping mechanism. But if the team didn't label consistently, there's no way to discover related MRs by content similarity. No "related items" view that combines issues + MRs + discussions for a topic.
|
||||
|
||||
---
|
||||
|
||||
### H14. Checking if a Similar Fix Was Already Attempted
|
||||
|
||||
**Problem:** "Before I implement this fix, was something similar tried before?"
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore search "memory leak connection pool" # Semantic search
|
||||
lore search "connection pool" --type mr -s all # Wait, no state filter on search
|
||||
lore mrs -s closed -l bugfix # Closed bugfix MRs (coarse)
|
||||
lore timeline "connection pool" # Historical context
|
||||
```
|
||||
|
||||
**Gap identified:** Search doesn't have a `--state` filter. Can't search only closed/merged items. The semantic search is powerful but can't be combined with entity state. Would benefit from `--state merged` on search to find past attempts.
|
||||
|
||||
---
|
||||
|
||||
### H15. Reviewing Discussions That Need My Attention
|
||||
|
||||
**Problem:** "Which discussion threads am I involved in that are still unresolved?"
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore who --active # All active unresolved discussions
|
||||
lore who --active --since 30d # Wider window
|
||||
# But can't filter to "discussions I'm in"...
|
||||
```
|
||||
|
||||
**Gap identified:** `--active` shows all unresolved discussions, not filtered by participant. No way to say "show me discussions where @me participated." No notification/mention tracking. No "my unresolved threads" view.
|
||||
|
||||
---
|
||||
|
||||
## Part 2: AI Agent Flows
|
||||
|
||||
### A1. Context Gathering Before Code Modification
|
||||
|
||||
**Problem:** Agent is about to modify `src/features/auth/session.rs` and needs full context.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J health # Pre-flight check
|
||||
lore -J who src/features/auth/ # Who knows this area
|
||||
lore -J search "auth session" -n 10 # Related issues/MRs
|
||||
lore -J mrs -s merged --since 3m -l auth # Recent auth changes
|
||||
lore -J who --overlap src/features/auth/session.rs # Concurrent work risk
|
||||
```
|
||||
|
||||
**Gap identified:** No way to check "are there open MRs touching this file right now?" The overlap mode shows historical touches, not active branches. An agent needs to know about in-flight changes to avoid conflicts.
|
||||
|
||||
---
|
||||
|
||||
### A2. Auto-Triaging an Incoming Issue
|
||||
|
||||
**Problem:** Agent receives a new issue and needs to categorize it, find related work, and suggest assignees.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J issues 999 # Read the new issue
|
||||
lore -J search "$(extract_keywords)" --explain # Find similar past issues
|
||||
lore -J who src/affected/path/ # Suggest experts as assignees
|
||||
lore -J issues -s opened -l same-label # Check for duplicates
|
||||
```
|
||||
|
||||
**Gap identified:** No way to get just the description text for programmatic keyword extraction. `issues <iid>` returns full detail including discussions. Agent must parse the full response to extract the description for a secondary search. Would benefit from `--fields description` on detail view. No duplicate detection built in.
|
||||
|
||||
---
|
||||
|
||||
### A3. Generating Sprint Status Report
|
||||
|
||||
**Problem:** Agent needs to produce a weekly status report for the team.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J issues -s closed --since 1w --fields minimal # Completed work
|
||||
lore -J issues -s opened --status "In progress" # In-flight work
|
||||
lore -J mrs -s merged --since 1w --fields minimal # Merged PRs
|
||||
lore -J mrs -s opened -D --fields minimal # Open non-draft MRs
|
||||
lore -J count issues # Totals
|
||||
lore -J count mrs # MR totals
|
||||
lore -J who --active --since 1w # Discussions needing attention
|
||||
```
|
||||
|
||||
**Gap identified:** Seven separate queries for one report. No `lore summary` or `lore report` command. No way to get "issues transitioned from X to Y this week" (state change history exists in events but isn't queryable). No velocity metric (issues closed per week trend).
|
||||
|
||||
---
|
||||
|
||||
### A4. Finding Relevant Prior Art Before Implementing
|
||||
|
||||
**Problem:** Agent is implementing a caching layer and wants to find if similar patterns exist in the codebase's GitLab history.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J search "caching" --mode hybrid -n 20 --explain
|
||||
lore -J search "cache invalidation" --mode hybrid -n 10
|
||||
lore -J search "redis" --mode lexical --type discussion # Exact term in discussions
|
||||
lore -J timeline "cache" --since 1y # Wait, max is 1y? Let's try 12m
|
||||
```
|
||||
|
||||
**Gap identified:** No way to search discussion notes individually (per-note search). Discussions are aggregated into documents, so individual note-level matches are lost. The `--explain` flag helps but doesn't show which specific note matched. No `--since 1y` or `--since 12m` duration format.
|
||||
|
||||
---
|
||||
|
||||
### A5. Building Context for PR Description
|
||||
|
||||
**Problem:** Agent wrote code and needs to generate a PR description that references relevant issues.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J search "feature description keywords" --type issue
|
||||
lore -J issues -s opened -l feature-label --fields iid,title,web_url
|
||||
# Cross-reference: which issues does this MR close?
|
||||
# No command for this -- must manually scan search results
|
||||
```
|
||||
|
||||
**Gap identified:** No way to query the `entity_references` table directly. Agent can't ask "which issues reference MR !456" or "which issues contain 'closes #123' in their text." The data exists but isn't exposed as a query surface. Would benefit from `lore refs --mr 456` or `lore refs --issue 123`.
|
||||
|
||||
---
|
||||
|
||||
### A6. Identifying Affected Experts for Review Assignment
|
||||
|
||||
**Problem:** Agent needs to automatically assign reviewers based on the files changed in an MR.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J mrs 456 # Get MR details
|
||||
# Parse file paths from response... but file changes aren't in the output
|
||||
lore -J who src/path/from/mr/ # Query each path
|
||||
lore -J who src/another/path/ # One at a time...
|
||||
lore -J who @candidate --fields minimal # Check workload
|
||||
```
|
||||
|
||||
**Gap identified:** MR detail view (`mrs <iid>`) doesn't include the file change list from `mr_file_changes`. Agent can't programmatically extract which files an MR touches. Must fall back to GitLab API or guess from description. The `who` command doesn't accept multiple paths. No "auto-reviewer" suggestion combining expertise + availability.
|
||||
|
||||
---
|
||||
|
||||
### A7. Incident Investigation and Timeline Reconstruction
|
||||
|
||||
**Problem:** Agent needs to reconstruct what happened during an outage for a postmortem.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J timeline "outage" --since 3d --depth 2 --expand-mentions
|
||||
lore -J search "error 500" --since 3d
|
||||
lore -J mrs -s merged --since 3d -p production-service
|
||||
lore -J issues --status "In progress" -p production-service
|
||||
```
|
||||
|
||||
**Gap identified:** Timeline is keyword-seeded, which means if the outage wasn't described with that exact term, seeds may miss it. No way to seed a timeline from an entity ID (e.g., "start from issue #321 and expand outward"). No severity/priority filter. No way to correlate with merge times.
|
||||
|
||||
---
|
||||
|
||||
### A8. Cross-Project Impact Assessment
|
||||
|
||||
**Problem:** Agent needs to understand how a breaking API change in project A affects projects B and C.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J search "api-endpoint-name" -p project-a
|
||||
lore -J search "api-endpoint-name" -p project-b
|
||||
lore -J search "api-endpoint-name" -p project-c
|
||||
# Or without project filter to search everywhere:
|
||||
lore -J search "api-endpoint-name" -n 50
|
||||
lore -J timeline "api-endpoint-name" --depth 2
|
||||
```
|
||||
|
||||
**Gap identified:** Cross-project references in entity_references are tracked but the timeline shows unresolved references for entities not synced locally. No way to see a cross-project dependency map. Search works across projects but doesn't group results by project.
|
||||
|
||||
---
|
||||
|
||||
### A9. Automated Stale Issue Recommendations
|
||||
|
||||
**Problem:** Agent runs weekly to identify issues that should be closed or re-prioritized.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J issues -s opened --sort updated --asc -n 100 # Oldest first
|
||||
# For each issue, check:
|
||||
lore -J issues <iid> # Read details
|
||||
lore -J search "<issue title keywords>" # Any recent activity?
|
||||
```
|
||||
|
||||
**Gap identified:** No `--updated-before` filter, so agent must fetch all and filter client-side. No way to detect "issue has no assignee AND no activity in 90 days." The 100-issue limit means pagination is needed for large backlogs, but there's no cursor/offset pagination -- only `--limit`. Agent must do N+1 queries to inspect each candidate.
|
||||
|
||||
---
|
||||
|
||||
### A10. Code Review Preparation (File-Level Context)
|
||||
|
||||
**Problem:** Agent is reviewing MR !789 and needs to understand the history of each changed file.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J mrs 789 # Get MR details
|
||||
# Can't get file list from output...
|
||||
# Fall back to search by MR title keywords
|
||||
lore -J search "feature-from-mr" --type mr
|
||||
lore -J who src/guessed/path/ # Expertise for each file
|
||||
lore -J who --overlap src/guessed/path/ # Concurrent changes
|
||||
```
|
||||
|
||||
**Gap identified:** Same as A6 -- `mr_file_changes` data isn't exposed. Agent is blind to the actual files in the MR unless it parses the description or uses the GitLab API directly. This is the single biggest gap for automated code review workflows.
|
||||
|
||||
---
|
||||
|
||||
### A11. Building a Knowledge Graph of Entity Relationships
|
||||
|
||||
**Problem:** Agent wants to map how issues, MRs, and discussions are connected for a feature.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J search "feature-name" -n 30
|
||||
lore -J timeline "feature-name" --depth 2 --max-entities 100
|
||||
# Timeline shows expanded entities and cross-refs, but...
|
||||
# No way to query entity_references directly
|
||||
# No way to get "all entities that reference issue #123"
|
||||
```
|
||||
|
||||
**Gap identified:** The `entity_references` table (closes, related, mentioned) is used internally by timeline but isn't queryable as a standalone command. Agent can't ask "what closes issue #123?" or "what does MR !456 reference?" No graph export. Would enable powerful dependency mapping.
|
||||
|
||||
---
|
||||
|
||||
### A12. Release Readiness Assessment
|
||||
|
||||
**Problem:** Agent needs to verify all issues in milestone "v2.0" are closed and MRs are merged.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J issues -m "v2.0" -s opened # Any open issues in milestone?
|
||||
lore -J issues -m "v2.0" -s closed # Closed issues
|
||||
# MRs don't have milestone filter...
|
||||
lore -J mrs -s opened -l "v2.0" # Try label as proxy
|
||||
lore -J who --active -p myproject # Unresolved discussions
|
||||
```
|
||||
|
||||
**Gap identified:** MRs don't have a `--milestone` filter (issues do). No way to check "all MRs linked to issues in milestone v2.0" -- would require joining `entity_references` with issue milestone. No release checklist concept. No way to verify "every issue in this milestone has a closing MR."
|
||||
|
||||
---
|
||||
|
||||
### A13. Answering "What Changed?" Between Two Points
|
||||
|
||||
**Problem:** Agent needs to diff project state between two dates for a stakeholder report.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J issues -s closed --since 2w --fields minimal # Recently closed
|
||||
lore -J issues -s opened --since 2w --fields minimal # Recently opened
|
||||
lore -J mrs -s merged --since 2w --fields minimal # Recently merged
|
||||
# But no way to get "issues that CHANGED STATE" in a window
|
||||
# An issue opened 3 months ago but closed yesterday won't appear in --since 2w for issues -s opened
|
||||
```
|
||||
|
||||
**Gap identified:** `--since` filters by `updated_at`, not by "state changed at." An issue closed yesterday but created 6 months ago would appear in `issues -s closed --since 1d` (because updated_at changed), but the semantics are subtle. No explicit "state transitions in time window" query. The resource_state_events table has this data but it's not exposed as a filter.
|
||||
|
||||
---
|
||||
|
||||
### A14. Meeting Prep: Summarize Recent Activity for a Stakeholder
|
||||
|
||||
**Problem:** Agent needs to prepare a 2-minute summary for a project sponsor meeting.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J count issues -p project # Current totals
|
||||
lore -J count mrs -p project # MR totals
|
||||
lore -J issues -s closed --since 1w -p project --fields minimal
|
||||
lore -J mrs -s merged --since 1w -p project --fields minimal
|
||||
lore -J issues -s opened --status "In progress" -p project
|
||||
lore -J who --active -p project --since 1w
|
||||
```
|
||||
|
||||
**Gap identified:** Six queries, same as A3. No summary/dashboard command. Agent must synthesize all responses. No trend data (is the open issue count growing or shrinking?). No "highlights" extraction.
|
||||
|
||||
---
|
||||
|
||||
### A15. Determining If Work Is Safe to Start (Conflict Detection)
|
||||
|
||||
**Problem:** Agent is about to start work on an issue and needs to check nobody else is already working on it.
|
||||
|
||||
**Flow:**
|
||||
```
|
||||
lore -J issues 123 # Read the issue
|
||||
# Check assignees from response
|
||||
lore -J mrs -s opened -A other-person # Are they working on related MRs?
|
||||
lore -J who --overlap src/target/path/ # Anyone actively touching these files?
|
||||
lore -J search "issue-123-keywords" --type mr -s opened # Wait, search has no --state
|
||||
```
|
||||
|
||||
**Gap identified:** No way to check "is there an open MR that closes issue #123?" -- the entity_references data exists but isn't queryable. Search doesn't support `--state` filter. No "conflict detection" or "in-flight work" check. Agent must do multiple queries and manually correlate.
|
||||
|
||||
---
|
||||
|
||||
## Part 3: Gap Summary
|
||||
|
||||
### Critical Gaps (high impact, blocks common workflows)
|
||||
|
||||
| # | Gap | Affected Flows | Suggested Command/Flag |
|
||||
|---|-----|----------------|----------------------|
|
||||
| 1 | **MR file changes not surfaced** | H4, A6, A10 | `lore mrs <iid> --files` or include in detail view |
|
||||
| 2 | **Entity references not queryable** | H7, A5, A11, A15 | `lore refs --issue 123` / `lore refs --mr 456` |
|
||||
| 3 | **Per-note search missing** | H7, A4 | `lore search --granularity note` (PRD exists) |
|
||||
| 4 | **No entity-based timeline** | H11, A7 | `lore timeline --issue 321` / `lore timeline --mr 456` |
|
||||
| 5 | **No @me / current-user alias** | H1, H15 | Resolve from auth token automatically |
|
||||
|
||||
### Important Gaps (significant friction, multiple workarounds needed)
|
||||
|
||||
| # | Gap | Affected Flows | Suggested Command/Flag |
|
||||
|---|-----|----------------|----------------------|
|
||||
| 6 | **No activity feed / summary** | H1, A3, A14 | `lore activity --since 1d` or `lore summary` |
|
||||
| 7 | **No multi-path who query** | H6, A6 | `lore who src/path1/ src/path2/` |
|
||||
| 8 | **No --state filter on search** | H14, A15 | `lore search --state merged` |
|
||||
| 9 | **MRs missing --milestone filter** | H9, A12 | `lore mrs -m "v2.0"` |
|
||||
| 10 | **No --no-assignee / --unassigned** | H2 | `lore issues --no-assignee` |
|
||||
| 11 | **No --updated-before filter** | H10, A9 | `lore issues --before 90d` or `--stale 90d` |
|
||||
| 12 | **No team workload view** | H8 | `lore who --team` or `lore workload` |
|
||||
|
||||
### Nice-to-Have Gaps (would improve agent efficiency)
|
||||
|
||||
| # | Gap | Affected Flows | Suggested Command/Flag |
|
||||
|---|-----|----------------|----------------------|
|
||||
| 13 | **No pagination/offset** | A9 | `--offset 100` for large result sets |
|
||||
| 14 | **No detail --fields on show** | A2 | `lore issues 999 --fields description` |
|
||||
| 15 | **No cross-project grouping** | A8 | `lore search --group-by project` |
|
||||
| 16 | **No trend/velocity metrics** | A3, A14 | `lore trends issues --period week` |
|
||||
| 17 | **No --for-issue on mrs** | A12, A15 | `lore mrs --closes 123` (query entity_refs) |
|
||||
| 18 | **1y/12m duration not supported** | A4 | Support `1y`, `12m`, `365d` in --since |
|
||||
| 19 | **No discussion participant filter** | H15 | `lore who --active --participant @me` |
|
||||
| 20 | **No sort by due date** | H2 | `lore issues --sort due` |
|
||||
9
migrations/021_work_item_status.sql
Normal file
9
migrations/021_work_item_status.sql
Normal file
@@ -0,0 +1,9 @@
|
||||
ALTER TABLE issues ADD COLUMN status_name TEXT;
|
||||
ALTER TABLE issues ADD COLUMN status_category TEXT;
|
||||
ALTER TABLE issues ADD COLUMN status_color TEXT;
|
||||
ALTER TABLE issues ADD COLUMN status_icon_name TEXT;
|
||||
ALTER TABLE issues ADD COLUMN status_synced_at INTEGER;
|
||||
CREATE INDEX IF NOT EXISTS idx_issues_project_status_name ON issues(project_id, status_name);
|
||||
|
||||
INSERT INTO schema_version (version, applied_at, description)
|
||||
VALUES (21, strftime('%s', 'now') * 1000, 'Work item status columns for issues');
|
||||
5
migrations/023_issue_detail_fields.sql
Normal file
5
migrations/023_issue_detail_fields.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
ALTER TABLE issues ADD COLUMN closed_at TEXT;
|
||||
ALTER TABLE issues ADD COLUMN confidential INTEGER NOT NULL DEFAULT 0;
|
||||
|
||||
INSERT INTO schema_version (version, applied_at, description)
|
||||
VALUES (23, strftime('%s', 'now') * 1000, 'Add closed_at and confidential to issues');
|
||||
186
plans/lore-service.feedback-1.md
Normal file
186
plans/lore-service.feedback-1.md
Normal file
@@ -0,0 +1,186 @@
|
||||
1. **Isolate scheduled behavior from manual `sync`**
|
||||
Reasoning: Your current plan injects backoff into `handle_sync_cmd`, which affects all `lore sync` calls (including manual recovery runs). Scheduled behavior should be isolated so humans aren’t unexpectedly blocked by service backoff.
|
||||
|
||||
```diff
|
||||
@@ Context
|
||||
-`lore sync` runs a 4-stage pipeline (issues, MRs, docs, embeddings) that takes 2-4 minutes.
|
||||
+`lore sync` remains the manual/operator command.
|
||||
+`lore service run` (hidden/internal) is the scheduled execution entrypoint.
|
||||
|
||||
@@ Commands & User Journeys
|
||||
+### `lore service run` (hidden/internal)
|
||||
+**What it does:** Executes one scheduled sync attempt with service-only policy:
|
||||
+- applies service backoff policy
|
||||
+- records service run state
|
||||
+- invokes sync pipeline with configured profile
|
||||
+- updates retry state on success/failure
|
||||
+
|
||||
+**Invocation:** scheduler always runs:
|
||||
+`lore --robot service run --reason timer`
|
||||
|
||||
@@ Backoff Integration into `handle_sync_cmd`
|
||||
-Insert **after** config load but **before** the dry_run check:
|
||||
+Do not add backoff checks to `handle_sync_cmd`.
|
||||
+Backoff logic lives only in `handle_service_run`.
|
||||
```
|
||||
|
||||
2. **Use DB as source-of-truth for service state (not a standalone JSON status file)**
|
||||
Reasoning: You already have `sync_runs` in SQLite. A separate JSON status file creates split-brain and race/corruption risk. Keep JSON as optional cache/export only.
|
||||
|
||||
```diff
|
||||
@@ Status File
|
||||
-Location: `{get_data_dir()}/sync-status.json`
|
||||
+Primary state location: SQLite (`service_state` table) + existing `sync_runs`.
|
||||
+Optional mirror file: `{get_data_dir()}/sync-status.json` (best-effort export only).
|
||||
|
||||
@@ File-by-File Implementation Details
|
||||
-### `src/core/sync_status.rs` (NEW)
|
||||
+### `migrations/015_service_state.sql` (NEW)
|
||||
+CREATE TABLE service_state (
|
||||
+ id INTEGER PRIMARY KEY CHECK (id = 1),
|
||||
+ installed INTEGER NOT NULL DEFAULT 0,
|
||||
+ platform TEXT,
|
||||
+ interval_seconds INTEGER,
|
||||
+ profile TEXT NOT NULL DEFAULT 'balanced',
|
||||
+ consecutive_failures INTEGER NOT NULL DEFAULT 0,
|
||||
+ next_retry_at_ms INTEGER,
|
||||
+ last_error_code TEXT,
|
||||
+ last_error_message TEXT,
|
||||
+ updated_at_ms INTEGER NOT NULL
|
||||
+);
|
||||
+
|
||||
+### `src/core/service_state.rs` (NEW)
|
||||
+- read/write state row
|
||||
+- derive backoff/next_retry
|
||||
+- join with latest `sync_runs` for status output
|
||||
```
|
||||
|
||||
3. **Backoff policy should be configurable, jittered, and error-aware**
|
||||
Reasoning: Fixed hardcoded backoff (`base=1800`) is wrong when user sets another interval. Also permanent failures (bad token/config) should not burn retries forever; they should enter paused/error state.
|
||||
|
||||
```diff
|
||||
@@ Backoff Logic
|
||||
-// Exponential: base * 2^failures, capped at 4 hours
|
||||
+// Exponential with jitter: base * 2^(failures-1), capped, ±20% jitter
|
||||
+// Applies only to transient errors.
|
||||
+// Permanent errors set `paused_reason` and stop retries until user action.
|
||||
|
||||
@@ CLI Definition Changes
|
||||
+ServiceCommand::Resume, // clear paused state / failures
|
||||
+ServiceCommand::Run, // hidden
|
||||
|
||||
@@ Error Types
|
||||
+ServicePaused, // scheduler paused due to permanent error
|
||||
+ServiceCommandFailed, // OS command failure with stderr context
|
||||
```
|
||||
|
||||
4. **Add a pipeline-level single-flight lock**
|
||||
Reasoning: Current locking is in ingest stages; there’s still overlap risk across full sync pipelines (docs/embed can overlap with another run). Add a top-level lock for scheduled/manual sync pipeline execution.
|
||||
|
||||
```diff
|
||||
@@ Architecture
|
||||
+Add `sync_pipeline` lock at top-level sync execution.
|
||||
+Keep existing ingest lock (`sync`) for ingest internals.
|
||||
|
||||
@@ Backoff Integration into `handle_sync_cmd`
|
||||
+Before starting sync pipeline, acquire `AppLock` with:
|
||||
+name = "sync_pipeline"
|
||||
+stale_lock_minutes = config.sync.stale_lock_minutes
|
||||
+heartbeat_interval_seconds = config.sync.heartbeat_interval_seconds
|
||||
```
|
||||
|
||||
5. **Don’t embed token in service files by default**
|
||||
Reasoning: Embedding PAT into unit/plist is a high-risk secret leak path. Make secure storage explicit and default-safe.
|
||||
|
||||
```diff
|
||||
@@ `lore service install [--interval 30m]`
|
||||
+`lore service install [--interval 30m] [--token-source env-file|embedded]`
|
||||
+Default: `env-file` (0600 perms, user-owned)
|
||||
+`embedded` allowed only with explicit opt-in and warning
|
||||
|
||||
@@ Robot output
|
||||
- "token_embedded": true
|
||||
+ "token_source": "env_file"
|
||||
|
||||
@@ Human output
|
||||
- Note: Your GITLAB_TOKEN is embedded in the service file.
|
||||
+ Note: Token is stored in a user-private env file (0600).
|
||||
```
|
||||
|
||||
6. **Introduce a command-runner abstraction with timeout + stderr capture**
|
||||
Reasoning: `launchctl/systemctl/schtasks` calls are failure-prone; you need consistent error mapping and deterministic tests.
|
||||
|
||||
```diff
|
||||
@@ Platform Backends
|
||||
-exports free functions that dispatch via `#[cfg(target_os)]`
|
||||
+exports backend + shared `CommandRunner`:
|
||||
+- run(cmd, args, timeout)
|
||||
+- capture stdout/stderr/exit code
|
||||
+- map failure to `ServiceCommandFailed { cmd, exit_code, stderr }`
|
||||
```
|
||||
|
||||
7. **Persist install manifest to avoid brittle file parsing**
|
||||
Reasoning: Parsing timer/plist for interval/state is fragile and platform-format dependent. Persist a manifest with checksums and expected artifacts.
|
||||
|
||||
```diff
|
||||
@@ Platform Backends
|
||||
-Same pattern for ... `get_interval_seconds()`
|
||||
+Add manifest: `{data_dir}/service-manifest.json`
|
||||
+Stores platform, interval, profile, generated files, and command.
|
||||
+`service status` reads manifest first, then verifies platform state.
|
||||
|
||||
@@ Acceptance criteria
|
||||
+Install is idempotent:
|
||||
+- if manifest+files already match, report `no_change: true`
|
||||
+- if drift detected, reconcile and rewrite
|
||||
```
|
||||
|
||||
8. **Make schedule profile explicit (`fast|balanced|full`)**
|
||||
Reasoning: This makes the feature more useful and performance-tunable without requiring users to understand internal flags.
|
||||
|
||||
```diff
|
||||
@@ `lore service install [--interval 30m]`
|
||||
+`lore service install [--interval 30m] [--profile fast|balanced|full]`
|
||||
+
|
||||
+Profiles:
|
||||
+- fast: `sync --no-docs --no-embed`
|
||||
+- balanced (default): `sync --no-embed`
|
||||
+- full: `sync`
|
||||
```
|
||||
|
||||
9. **Upgrade `service status` to include scheduler health + recent run summary**
|
||||
Reasoning: Single last-sync snapshot is too shallow. Include recent attempts and whether scheduler is paused/backing off/running.
|
||||
|
||||
```diff
|
||||
@@ `lore service status`
|
||||
-What it does: Shows whether the service is installed, its configuration, last sync result, and next scheduled run.
|
||||
+What it does: Shows install state, scheduler state (running/backoff/paused), recent runs, and next run estimate.
|
||||
|
||||
@@ Robot output
|
||||
- "last_sync": { ... },
|
||||
- "backoff": null
|
||||
+ "scheduler_state": "running|backoff|paused|idle",
|
||||
+ "last_sync": { ... },
|
||||
+ "recent_runs": [{"run_id":"...","status":"...","started_at_iso":"..."}],
|
||||
+ "backoff": null,
|
||||
+ "paused_reason": null
|
||||
```
|
||||
|
||||
10. **Strengthen tests around determinism and cross-platform generation**
|
||||
Reasoning: Time-based backoff and shell quoting are classic flaky points. Add fake clock + fake command runner for deterministic tests.
|
||||
|
||||
```diff
|
||||
@@ Testing Strategy
|
||||
+Add deterministic test seams:
|
||||
+- `Clock` trait for backoff/now calculations
|
||||
+- `CommandRunner` trait for backend command execution
|
||||
+
|
||||
+Add tests:
|
||||
+- transient vs permanent error classification
|
||||
+- backoff schedule with jitter bounds
|
||||
+- manifest drift reconciliation
|
||||
+- quoting/escaping for paths with spaces and special chars
|
||||
+- `service run` does not modify manual `sync` behavior
|
||||
```
|
||||
|
||||
If you want, I can rewrite your full plan as a single clean revised document with these changes already integrated (instead of patch fragments).
|
||||
182
plans/lore-service.feedback-2.md
Normal file
182
plans/lore-service.feedback-2.md
Normal file
@@ -0,0 +1,182 @@
|
||||
**High-Impact Revisions (ordered by priority)**
|
||||
|
||||
1. **Make service identity project-scoped (avoid collisions across repos/users)**
|
||||
Analysis: Current fixed names (`com.gitlore.sync`, `LoreSync`, `lore-sync.timer`) will collide when users run multiple gitlore workspaces. This causes silent overwrites and broken uninstall/status behavior.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Commands & User Journeys / install
|
||||
- lore service install [--interval 30m] [--profile balanced] [--token-source env-file]
|
||||
+ lore service install [--interval 30m] [--profile balanced] [--token-source auto] [--name <optional>]
|
||||
@@ Install Manifest Schema
|
||||
+ /// Stable per-install identity (default derived from project root hash)
|
||||
+ pub service_id: String,
|
||||
@@ Platform Backends
|
||||
- Label: com.gitlore.sync
|
||||
+ Label: com.gitlore.sync.{service_id}
|
||||
- Task name: LoreSync
|
||||
+ Task name: LoreSync-{service_id}
|
||||
- ~/.config/systemd/user/lore-sync.service
|
||||
+ ~/.config/systemd/user/lore-sync-{service_id}.service
|
||||
```
|
||||
|
||||
2. **Replace token model with secure per-OS defaults**
|
||||
Analysis: The current “env-file default” is not actually secure on macOS launchd (token still ends up in plist). On Windows, assumptions about inherited environment are fragile. Use OS-native secure stores by default and keep `embedded` as explicit opt-in only.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Token storage strategies
|
||||
-| env-file (default) | ...
|
||||
+| auto (default) | macOS: Keychain, Linux: env-file (0600), Windows: Credential Manager |
|
||||
+| env-file | Linux/systemd only |
|
||||
| embedded | ... explicit warning ...
|
||||
@@ macOS launchd section
|
||||
- env-file strategy stores canonical token in service-env but embeds token in plist
|
||||
+ default strategy is Keychain lookup at runtime; no token persisted in plist
|
||||
+ env-file is not offered on macOS
|
||||
@@ Windows schtasks section
|
||||
- token must be in user's system environment
|
||||
+ default strategy stores token in Windows Credential Manager and injects at runtime
|
||||
```
|
||||
|
||||
3. **Version and atomically persist manifest/status**
|
||||
Analysis: `Option<Self>` on read hides corruption, and non-atomic writes risk truncated JSON on crashes. This will create false “not installed” and scheduler confusion.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Install Manifest Schema
|
||||
+ pub schema_version: u32, // start at 1
|
||||
+ pub updated_at_iso: String,
|
||||
@@ Status File Schema
|
||||
+ pub schema_version: u32, // start at 1
|
||||
+ pub updated_at_iso: String,
|
||||
@@ Read/Write
|
||||
- read(path) -> Option<Self>
|
||||
+ read(path) -> Result<Option<Self>, LoreError>
|
||||
- write(...) -> std::io::Result<()>
|
||||
+ write_atomic(...) -> std::io::Result<()> // tmp file + fsync + rename
|
||||
```
|
||||
|
||||
4. **Persist `next_retry_at_ms` instead of recomputing jitter**
|
||||
Analysis: Deterministic jitter from timestamp modulo is predictable and can herd retries. Persisting `next_retry_at_ms` at failure time makes status accurate, stable, and cheap to compute.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ SyncStatusFile
|
||||
pub consecutive_failures: u32,
|
||||
+ pub next_retry_at_ms: Option<i64>,
|
||||
@@ Backoff Logic
|
||||
- compute backoff from last_run.timestamp_ms and deterministic jitter each read
|
||||
+ compute backoff once on failure, store next_retry_at_ms, read-only comparison afterward
|
||||
+ jitter algorithm: full jitter in [0, cap], injectable RNG for tests
|
||||
```
|
||||
|
||||
5. **Add circuit breaker for repeated transient failures**
|
||||
Analysis: Infinite transient retries can run forever on systemic failures (DB corruption, bad network policy). After N transient failures, pause with actionable reason.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Scheduler states
|
||||
- backoff — transient failures, waiting to retry
|
||||
+ backoff — transient failures, waiting to retry
|
||||
+ paused — permanent error OR circuit breaker tripped after N transient failures
|
||||
@@ Service run flow
|
||||
- On transient failure: increment failures, compute backoff
|
||||
+ On transient failure: increment failures, compute backoff, if failures >= max_transient_failures -> pause
|
||||
```
|
||||
|
||||
6. **Stage-aware outcome policy (core freshness over all-or-nothing)**
|
||||
Analysis: Failing embeddings/docs should not block issues/MRs freshness. Split stage outcomes and only treat core stages as hard-fail by default. This improves reliability and practical usefulness.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Context
|
||||
- lore sync runs a 4-stage pipeline ... treated as one run result
|
||||
+ lore service run records per-stage outcomes (issues, mrs, docs, embeddings)
|
||||
@@ Status File Schema
|
||||
+ pub stage_results: Vec<StageResult>,
|
||||
@@ service run flow
|
||||
- Execute sync pipeline with flags derived from profile
|
||||
+ Execute stage-by-stage and classify severity:
|
||||
+ - critical: issues, mrs
|
||||
+ - optional: docs, embeddings
|
||||
+ optional stage failures mark run as degraded, not failed
|
||||
```
|
||||
|
||||
7. **Replace cfg free-function backend with trait-based backend**
|
||||
Analysis: Current backend API is hard to test end-to-end without real OS commands. A `SchedulerBackend` trait enables deterministic integration tests and cleaner architecture.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Platform Backends / Architecture
|
||||
- exports free functions dispatched via #[cfg]
|
||||
+ define trait SchedulerBackend { install, uninstall, state, file_paths, next_run }
|
||||
+ provide LaunchdBackend, SystemdBackend, SchtasksBackend implementations
|
||||
+ include FakeBackend for integration tests
|
||||
```
|
||||
|
||||
8. **Harden platform units and detect scheduler prerequisites**
|
||||
Analysis: systemd user timers often fail silently without user manager/linger; launchd context can be wrong in headless sessions. Add explicit diagnostics and unit hardening.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Linux systemd unit
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart=...
|
||||
+TimeoutStartSec=900
|
||||
+NoNewPrivileges=true
|
||||
+PrivateTmp=true
|
||||
+ProtectSystem=strict
|
||||
+ProtectHome=read-only
|
||||
@@ Linux install/status
|
||||
+ detect user manager availability and linger state; surface warning/action
|
||||
@@ macOS install/status
|
||||
+ detect non-GUI bootstrap context and return actionable error
|
||||
```
|
||||
|
||||
9. **Add operational commands: `trigger`, `doctor`, and non-interactive log tail**
|
||||
Analysis: `logs` opening an editor is weak for automation and incident response. Operators need a preflight and immediate controlled run.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ ServiceCommand
|
||||
+ Trigger, // run one attempt through service policy now
|
||||
+ Doctor, // validate scheduler, token, paths, permissions
|
||||
@@ logs
|
||||
- opens editor
|
||||
+ supports --tail <n> and --follow in human mode
|
||||
+ robot mode can return last_n lines optionally
|
||||
```
|
||||
|
||||
10. **Fix plan inconsistencies and edge-case correctness**
|
||||
Analysis: There are internal mismatches that will cause implementation drift.
|
||||
Diff:
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Interval Parsing
|
||||
- supports 's' suffix
|
||||
+ remove 's' suffix (acceptance only allows 5m..24h)
|
||||
@@ uninstall acceptance
|
||||
- removes ALL service files only
|
||||
+ explicitly also remove service-manifest and service-env (status/logs retained)
|
||||
@@ SyncStatusFile schema
|
||||
- pub last_run: SyncRunRecord
|
||||
+ pub last_run: Option<SyncRunRecord> // matches idle/no runs state
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
**Recommended Architecture Upgrade Summary**
|
||||
|
||||
The strongest improvement set is: **(1) project-scoped IDs, (2) secure token defaults, (3) atomic/versioned state, (4) persisted retry schedule + circuit breaker, (5) stage-aware outcomes**. That combination materially improves correctness, multi-repo safety, security, operability, and real-world reliability without changing your core manual-vs-scheduled separation principle.
|
||||
174
plans/lore-service.feedback-3.md
Normal file
174
plans/lore-service.feedback-3.md
Normal file
@@ -0,0 +1,174 @@
|
||||
Below are the highest-impact revisions I’d make, ordered by severity/ROI. These focus on correctness first, then security, then operability and UX.
|
||||
|
||||
1. **Fix multi-install ambiguity (`service_id` exists, but commands can’t target one explicitly)**
|
||||
Analysis: The plan introduces `service-manifest-{service_id}.json`, but `status/uninstall/resume/logs` have no selector. In a multi-workspace or multi-name install scenario, behavior becomes ambiguous and error-prone. Add explicit targeting plus discovery.
|
||||
```diff
|
||||
@@ ## Commands & User Journeys
|
||||
+### `lore service list`
|
||||
+Lists installed services discovered from `{data_dir}/service-manifest-*.json`.
|
||||
+Robot output includes `service_id`, `platform`, `interval_seconds`, `profile`, `installed_at_iso`.
|
||||
|
||||
@@ ### `lore service uninstall`
|
||||
-### `lore service uninstall`
|
||||
+### `lore service uninstall [--service <service_id|name>] [--all]`
|
||||
@@
|
||||
-2. CLI reads install manifest to find `service_id`
|
||||
+2. CLI resolves target service via `--service` or current-project-derived default.
|
||||
+3. If multiple candidates and no selector, return actionable error.
|
||||
|
||||
@@ ### `lore service status`
|
||||
-### `lore service status`
|
||||
+### `lore service status [--service <service_id|name>]`
|
||||
```
|
||||
|
||||
2. **Make status state service-scoped (not global)**
|
||||
Analysis: A single `sync-status.json` for all services causes cross-service contamination (pause/backoff/outcome from one profile affecting another). Keep lock global, but state per service.
|
||||
```diff
|
||||
@@ ## Status File
|
||||
-### Location
|
||||
-`{get_data_dir()}/sync-status.json`
|
||||
+### Location
|
||||
+`{get_data_dir()}/sync-status-{service_id}.json`
|
||||
|
||||
@@ ## Paths Module Additions
|
||||
-pub fn get_service_status_path() -> PathBuf {
|
||||
- get_data_dir().join("sync-status.json")
|
||||
+pub fn get_service_status_path(service_id: &str) -> PathBuf {
|
||||
+ get_data_dir().join(format!("sync-status-{service_id}.json"))
|
||||
}
|
||||
@@
|
||||
-Note: `sync-status.json` is NOT scoped by `service_id`
|
||||
+Note: status is scoped by `service_id`; lock remains global (`sync_pipeline`) to prevent overlapping writes.
|
||||
```
|
||||
|
||||
3. **Stop classifying permanence via string matching**
|
||||
Analysis: Matching `"401 Unauthorized"` in strings is brittle and will misclassify edge cases. Carry machine codes through stage results and classify by `ErrorCode` only.
|
||||
```diff
|
||||
@@ pub struct StageResult {
|
||||
- pub error: Option<String>,
|
||||
+ pub error: Option<String>,
|
||||
+ pub error_code: Option<String>, // e.g., AUTH_FAILED, NETWORK_ERROR
|
||||
}
|
||||
@@ Error classification helpers
|
||||
-fn is_permanent_error_message(msg: Option<&str>) -> bool { ...string contains... }
|
||||
+fn is_permanent_error_code(code: Option<&str>) -> bool {
|
||||
+ matches!(code, Some("TOKEN_NOT_SET" | "AUTH_FAILED" | "CONFIG_NOT_FOUND" | "CONFIG_INVALID" | "MIGRATION_FAILED"))
|
||||
+}
|
||||
```
|
||||
|
||||
4. **Install should be transactional (manifest written last)**
|
||||
Analysis: Current order writes manifest before scheduler enable. If enable fails, you persist a false “installed” state. Use two-phase install with rollback.
|
||||
```diff
|
||||
@@ ### `lore service install` User journey
|
||||
-9. CLI writes install manifest ...
|
||||
-10. CLI runs the platform-specific enable command
|
||||
+9. CLI runs the platform-specific enable command
|
||||
+10. On success, CLI writes install manifest atomically
|
||||
+11. On failure, CLI removes generated files and returns `ServiceCommandFailed`
|
||||
```
|
||||
|
||||
5. **Fix launchd token security gap (env-file currently still embeds token)**
|
||||
Analysis: Current “env-file” on macOS still writes token into plist, defeating the main security goal. Generate a private wrapper script that reads env file at runtime and execs `lore`.
|
||||
```diff
|
||||
@@ ### macOS: launchd
|
||||
-<key>ProgramArguments</key>
|
||||
-<array>
|
||||
- <string>{binary_path}</string>
|
||||
- <string>--robot</string>
|
||||
- <string>service</string>
|
||||
- <string>run</string>
|
||||
-</array>
|
||||
+<key>ProgramArguments</key>
|
||||
+<array>
|
||||
+ <string>{data_dir}/service-run-{service_id}.sh</string>
|
||||
+</array>
|
||||
@@
|
||||
-`env-file`: ... token value must still appear in plist ...
|
||||
+`env-file`: token never appears in plist; wrapper loads `{data_dir}/service-env-{service_id}` at runtime.
|
||||
```
|
||||
|
||||
6. **Improve backoff math and add half-open circuit recovery**
|
||||
Analysis: Current jitter + min clamp makes first retry deterministic and can over-pause. Also circuit-breaker requires manual resume forever. Add cooldown + half-open probe to self-heal.
|
||||
```diff
|
||||
@@ Backoff Logic
|
||||
-let backoff_secs = ((base_backoff as f64) * jitter_factor) as u64;
|
||||
-let backoff_secs = backoff_secs.max(base_interval_seconds);
|
||||
+let max_backoff = base_backoff;
|
||||
+let min_backoff = base_interval_seconds;
|
||||
+let span = max_backoff.saturating_sub(min_backoff);
|
||||
+let backoff_secs = min_backoff + ((span as f64) * jitter_factor) as u64;
|
||||
|
||||
@@ Scheduler states
|
||||
-- `paused` — permanent error ... OR circuit breaker tripped ...
|
||||
+- `paused` — permanent error requiring intervention
|
||||
+- `half_open` — probe state after circuit cooldown; one trial run allowed
|
||||
|
||||
@@ Circuit breaker
|
||||
-... transitions to `paused` ... Run: lore service resume
|
||||
+... transitions to `half_open` after cooldown (default 30m). Successful probe closes breaker automatically; failed probe returns to backoff/paused.
|
||||
```
|
||||
|
||||
7. **Promote backend trait to v1 (not v2) for deterministic integration tests**
|
||||
Analysis: This is a reliability-critical feature spanning OS schedulers. A trait abstraction now gives true behavior tests and safer refactors.
|
||||
```diff
|
||||
@@ ### Platform Backends
|
||||
-> Future architecture note: A `SchedulerBackend` trait ... for v2.
|
||||
+Adopt `SchedulerBackend` trait in v1 with real backends (`launchd/systemd/schtasks`) and `FakeBackend` for tests.
|
||||
+This enables deterministic install/uninstall/status/run-path integration tests without touching host scheduler.
|
||||
```
|
||||
|
||||
8. **Harden `run_cmd` timeout behavior**
|
||||
Analysis: If timeout occurs, child process must be killed and reaped. Otherwise you leak processes and can wedge repeated runs.
|
||||
```diff
|
||||
@@ fn run_cmd(...)
|
||||
-// Wait with timeout
|
||||
-let output = wait_with_timeout(output, timeout_secs)?;
|
||||
+// Wait with timeout; on timeout kill child and wait to reap
|
||||
+let output = wait_with_timeout_kill_and_reap(child, timeout_secs)?;
|
||||
```
|
||||
|
||||
9. **Add manual control commands (`pause`, `trigger`, `repair`)**
|
||||
Analysis: These are high-utility operational controls. `trigger` helps immediate sync without waiting interval. `pause` supports maintenance windows. `repair` avoids manual file deletion for corrupt state.
|
||||
```diff
|
||||
@@ pub enum ServiceCommand {
|
||||
+ /// Pause scheduled execution without uninstalling
|
||||
+ Pause { #[arg(long)] reason: Option<String> },
|
||||
+ /// Trigger an immediate one-off run using installed profile
|
||||
+ Trigger { #[arg(long)] ignore_backoff: bool },
|
||||
+ /// Repair corrupt manifest/status by backing up and reinitializing
|
||||
+ Repair { #[arg(long)] service: Option<String> },
|
||||
}
|
||||
```
|
||||
|
||||
10. **Make `logs` default non-interactive and add rotation policy**
|
||||
Analysis: Opening editor by default is awkward for automation/SSH and slower for normal diagnosis. Defaulting to `tail` is more practical; `--open` can preserve editor behavior.
|
||||
```diff
|
||||
@@ ### `lore service logs`
|
||||
-By default, opens in the user's preferred editor.
|
||||
+By default, prints last 100 lines to stdout.
|
||||
+Use `--open` to open editor.
|
||||
@@
|
||||
+Log rotation: rotate `service-stdout.log` / `service-stderr.log` at 10 MB, keep 5 files.
|
||||
```
|
||||
|
||||
11. **Remove destructive/shell-unsafe suggested action**
|
||||
Analysis: `actions(): ["rm {path}", ...]` is unsafe (shell injection + destructive guidance). Replace with safe command path.
|
||||
```diff
|
||||
@@ LoreError::actions()
|
||||
-Self::ServiceCorruptState { path, .. } => vec![&format!("rm {path}"), "lore service install"],
|
||||
+Self::ServiceCorruptState { .. } => vec!["lore service repair", "lore service install"],
|
||||
```
|
||||
|
||||
12. **Tighten scheduler units for real-world reliability**
|
||||
Analysis: Add explicit working directory and success-exit handling to reduce environment drift and edge failures.
|
||||
```diff
|
||||
@@ systemd service unit
|
||||
[Service]
|
||||
Type=oneshot
|
||||
ExecStart={binary_path} --robot service run
|
||||
+WorkingDirectory={data_dir}
|
||||
+SuccessExitStatus=0
|
||||
TimeoutStartSec=900
|
||||
```
|
||||
|
||||
If you want, I can produce a single consolidated “v3 plan” markdown with these revisions already merged into your original structure.
|
||||
190
plans/lore-service.feedback-4.md
Normal file
190
plans/lore-service.feedback-4.md
Normal file
@@ -0,0 +1,190 @@
|
||||
No `## Rejected Recommendations` section was present in the plan you shared, so the proposals below are all net-new.
|
||||
|
||||
1. **Make scheduled runs explicitly target a single service instance**
|
||||
Analysis: right now `service run` has no selector, but the plan supports multiple installed services. That creates ambiguity and incorrect manifest/status selection. This is the most important architectural fix.
|
||||
|
||||
```diff
|
||||
@@ `lore service install` What it does
|
||||
- runs `lore --robot service run` at the specified interval
|
||||
+ runs `lore --robot service run --service-id <service_id>` at the specified interval
|
||||
|
||||
@@ Robot output (`install`)
|
||||
- "sync_command": "/usr/local/bin/lore --robot service run",
|
||||
+ "sync_command": "/usr/local/bin/lore --robot service run --service-id a1b2c3d4",
|
||||
|
||||
@@ `ServiceCommand` enum
|
||||
- #[command(hide = true)]
|
||||
- Run,
|
||||
+ #[command(hide = true)]
|
||||
+ Run {
|
||||
+ /// Internal selector injected by scheduler backend
|
||||
+ #[arg(long, hide = true)]
|
||||
+ service_id: String,
|
||||
+ },
|
||||
|
||||
@@ `handle_service_run` signature
|
||||
-pub fn handle_service_run(start: std::time::Instant) -> Result<(), Box<dyn std::error::Error>>
|
||||
+pub fn handle_service_run(service_id: &str, start: std::time::Instant) -> Result<(), Box<dyn std::error::Error>>
|
||||
|
||||
@@ run flow step 1
|
||||
- Read install manifest
|
||||
+ Read install manifest for `service_id`
|
||||
```
|
||||
|
||||
2. **Strengthen `service_id` derivation to avoid cross-workspace collisions**
|
||||
Analysis: hashing config path alone can collide when many workspaces share one global config. Identity should represent what is being synced, not only where config lives.
|
||||
|
||||
```diff
|
||||
@@ Key Design Principles / Project-Scoped Service Identity
|
||||
- derive from a stable hash of the config file path
|
||||
+ derive from a stable fingerprint of:
|
||||
+ - canonical workspace root
|
||||
+ - normalized configured GitLab project URLs
|
||||
+ - canonical config path
|
||||
+ then take first 12 hex chars of SHA-256
|
||||
|
||||
@@ `compute_service_id`
|
||||
- Returns first 8 hex chars of SHA-256 of the canonical config path.
|
||||
+ Returns first 12 hex chars of SHA-256 of a canonical identity tuple
|
||||
+ (workspace_root + sorted project URLs + config_path).
|
||||
```
|
||||
|
||||
3. **Introduce a service-state machine with a dedicated admin lock**
|
||||
Analysis: install/uninstall/pause/resume/repair/status can race each other. A lock and explicit transition table prevents invalid states and file races.
|
||||
|
||||
```diff
|
||||
@@ New section: Service State Model
|
||||
+ All state mutations are serialized by `AppLock("service-admin-{service_id}")`.
|
||||
+ Legal transitions:
|
||||
+ - idle -> running -> success|degraded|backoff|paused
|
||||
+ - backoff -> running|paused
|
||||
+ - paused -> half_open|running (resume)
|
||||
+ - half_open -> running|paused
|
||||
+ Any invalid transition is rejected with `ServiceCorruptState`.
|
||||
|
||||
@@ `handle_install`, `handle_uninstall`, `handle_pause`, `handle_resume`, `handle_repair`
|
||||
+ Acquire `service-admin-{service_id}` before mutating manifest/status/service files.
|
||||
```
|
||||
|
||||
4. **Unify manual and scheduled sync execution behind one orchestrator**
|
||||
Analysis: the plan currently duplicates stage logic and error classification in `service run`, increasing drift risk. A shared orchestrator gives one authoritative pipeline behavior.
|
||||
|
||||
```diff
|
||||
@@ Key Design Principles
|
||||
+ #### 6. Single Sync Orchestrator
|
||||
+ Both `lore sync` and `lore service run` call `SyncOrchestrator`.
|
||||
+ Service mode adds policy (backoff/circuit-breaker); manual mode bypasses policy.
|
||||
|
||||
@@ Service Run Implementation
|
||||
- execute_sync_stages(&sync_args)
|
||||
+ SyncOrchestrator::run(SyncMode::Service { profile, policy })
|
||||
|
||||
@@ manual sync
|
||||
- separate pipeline path
|
||||
+ SyncOrchestrator::run(SyncMode::Manual { flags })
|
||||
```
|
||||
|
||||
5. **Add bounded in-run retries for transient core-stage failures**
|
||||
Analysis: single-shot failure handling will over-trigger backoff on temporary network blips. One short retry per core stage significantly improves freshness without much extra runtime.
|
||||
|
||||
```diff
|
||||
@@ Stage-aware execution
|
||||
+ Core stages (`issues`, `mrs`) get up to 1 immediate retry on transient errors
|
||||
+ (jittered 1-5s). Permanent errors are never retried.
|
||||
+ Optional stages keep best-effort semantics.
|
||||
|
||||
@@ Acceptance criteria (`service run`)
|
||||
+ Retries transient core stage failures once before counting run as failed.
|
||||
```
|
||||
|
||||
6. **Harden persistence with full crash-safety semantics**
|
||||
Analysis: current atomic write description is good but incomplete for power-loss durability. You should fsync parent directory after rename and include lightweight integrity metadata.
|
||||
|
||||
```diff
|
||||
@@ `write_atomic`
|
||||
- tmp file + fsync + rename
|
||||
+ tmp file + fsync(file) + rename + fsync(parent_dir)
|
||||
|
||||
@@ `ServiceManifest` and `SyncStatusFile`
|
||||
+ pub write_seq: u64,
|
||||
+ pub content_sha256: String, // optional integrity guard for repair/doctor
|
||||
```
|
||||
|
||||
7. **Fix token handling to avoid shell/env injection and add secure-store mode**
|
||||
Analysis: sourcing env files in shell is brittle if token contains special chars/newlines. Also, secure OS credential stores should be first-class for production reliability/security.
|
||||
|
||||
```diff
|
||||
@@ Token storage strategies
|
||||
-| `env-file` (default) ...
|
||||
+| `auto` (default) | use secure-store when available, else env-file |
|
||||
+| `secure-store` | macOS Keychain / libsecret / Windows Credential Manager |
|
||||
+| `env-file` | explicit fallback |
|
||||
|
||||
@@ macOS wrapper script
|
||||
-. "{data_dir}/service-env-{service_id}"
|
||||
-export {token_env_var}
|
||||
+TOKEN_VALUE="$(cat "{data_dir}/service-token-{service_id}" )"
|
||||
+export {token_env_var}="$TOKEN_VALUE"
|
||||
|
||||
@@ Acceptance criteria
|
||||
+ Reject token values containing `\0` or newline for env-file mode.
|
||||
+ Never eval/source untrusted token content.
|
||||
```
|
||||
|
||||
8. **Correct platform/runtime implementation hazards**
|
||||
Analysis: there are a few correctness risks that should be fixed in-plan now.
|
||||
|
||||
```diff
|
||||
@@ macOS install steps
|
||||
- Get UID via `unsafe { libc::getuid() }`
|
||||
+ Get UID via safe API (`nix::unistd::Uid::current()` or equivalent safe helper)
|
||||
|
||||
@@ Command Runner Helper
|
||||
- poll try_wait and read stdout/stderr after exit
|
||||
+ avoid potential pipe backpressure deadlock:
|
||||
+ use wait-with-timeout + concurrent stdout/stderr draining
|
||||
|
||||
@@ Linux timer
|
||||
- OnUnitActiveSec={interval_seconds}s
|
||||
+ OnUnitInactiveSec={interval_seconds}s
|
||||
+ AccuracySec=1min
|
||||
```
|
||||
|
||||
9. **Make logs fully service-scoped**
|
||||
Analysis: you already scoped manifest/status by `service_id`; logs are still global in several places. Multi-service installs will overwrite each other’s logs.
|
||||
|
||||
```diff
|
||||
@@ Paths Module Additions
|
||||
-pub fn get_service_log_path() -> PathBuf
|
||||
+pub fn get_service_log_path(service_id: &str, stream: LogStream) -> PathBuf
|
||||
|
||||
@@ log filenames
|
||||
- logs/service-stderr.log
|
||||
- logs/service-stdout.log
|
||||
+ logs/service-{service_id}-stderr.log
|
||||
+ logs/service-{service_id}-stdout.log
|
||||
|
||||
@@ `service logs`
|
||||
- default path: `{data_dir}/logs/service-stderr.log`
|
||||
+ default path: `{data_dir}/logs/service-{service_id}-stderr.log`
|
||||
```
|
||||
|
||||
10. **Resolve internal spec contradictions and rollback gaps**
|
||||
Analysis: there are a few contradictory statements and incomplete rollback behavior that will cause implementation churn.
|
||||
|
||||
```diff
|
||||
@@ `service logs` behavior
|
||||
- default (no flags): open in editor (human)
|
||||
+ default (no flags): print last 100 lines (human and robot metadata mode)
|
||||
+ `--open` is explicit opt-in
|
||||
|
||||
@@ install rollback
|
||||
- On failure: removes generated service files
|
||||
+ On failure: removes generated service files, env file, wrapper script, and temp manifest
|
||||
|
||||
@@ `handle_service_run` sample code
|
||||
- let manifest_path = get_service_manifest_path();
|
||||
+ let manifest_path = get_service_manifest_path(service_id);
|
||||
```
|
||||
|
||||
If you want, I can take these revisions and produce a single consolidated “Iteration 4” replacement plan block with all sections rewritten coherently so it’s ready to hand to an implementer.
|
||||
196
plans/lore-service.feedback-5.md
Normal file
196
plans/lore-service.feedback-5.md
Normal file
@@ -0,0 +1,196 @@
|
||||
I reviewed the full plan and avoided everything already listed in `## Rejected Recommendations`. These are the highest-impact revisions I’d make.
|
||||
|
||||
1. **Fix identity model inconsistency and prevent `--name` alias collisions**
|
||||
Why this improves the plan: your text says identity includes workspace root, but the current derivation code does not. Also, using `--name` as the actual `service_id` risks accidental cross-project collisions and destructive updates.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Key Design Principles / 2. Project-Scoped Service Identity
|
||||
- Each installed service gets a unique `service_id` derived from a canonical identity tuple: the config file path, sorted GitLab project URLs, and workspace root.
|
||||
+ Each installed service gets an immutable `identity_hash` derived from a canonical identity tuple:
|
||||
+ workspace root + canonical config path + sorted normalized project URLs.
|
||||
+ `service_id` remains the scheduler identifier; `--name` is a human alias only.
|
||||
+ If `--name` collides with an existing service that has a different `identity_hash`, install fails with an actionable error.
|
||||
|
||||
@@ Install Manifest / Schema
|
||||
+ /// Immutable identity hash for collision-safe matching across reinstalls
|
||||
+ pub identity_hash: String,
|
||||
+ /// Optional human-readable alias passed via --name
|
||||
+ #[serde(skip_serializing_if = "Option::is_none")]
|
||||
+ pub service_alias: Option<String>,
|
||||
+ /// Canonical workspace root used in identity derivation
|
||||
+ pub workspace_root: String,
|
||||
|
||||
@@ service_id derivation
|
||||
-pub fn compute_service_id(config_path: &Path, project_urls: &[&str]) -> String
|
||||
+pub fn compute_identity_hash(workspace_root: &Path, config_path: &Path, project_urls: &[&str]) -> String
|
||||
```
|
||||
|
||||
2. **Add lock protocol to eliminate uninstall/run race conditions**
|
||||
Why this improves the plan: today `service run` does not take admin lock, and admin commands do not take pipeline lock. `uninstall` can race with an active run and remove files mid-execution.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Key Design Principles / 6. Serialized Admin Mutations
|
||||
- The `service run` entrypoint does NOT acquire the admin lock — it only acquires the `sync_pipeline` lock
|
||||
+ The `service run` entrypoint acquires only `sync_pipeline`.
|
||||
+ Destructive admin operations (`install` overwrite, `uninstall`, `repair --regenerate`) must:
|
||||
+ 1) acquire `service-admin-{service_id}`
|
||||
+ 2) disable scheduler backend entrypoint
|
||||
+ 3) acquire `sync_pipeline` lock with timeout
|
||||
+ 4) mutate/remove files
|
||||
+ This lock ordering is mandatory to prevent deadlocks and run/delete races.
|
||||
|
||||
@@ lore service uninstall / User journey
|
||||
- 4. Runs platform-specific disable command
|
||||
- 5. Removes service files from disk
|
||||
+ 4. Acquires `sync_pipeline` lock (after disabling scheduler) with bounded wait
|
||||
+ 5. Removes service files from disk only after lock acquisition
|
||||
```
|
||||
|
||||
3. **Make transient handling `Retry-After` aware**
|
||||
Why this improves the plan: rate-limit and 503 responses often carry retry hints. Ignoring them causes useless retries and longer outages.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Transient vs permanent error classification
|
||||
-| Transient | Retry with backoff | Network timeout, rate limited, DB locked, 5xx from GitLab |
|
||||
+| Transient | Retry with adaptive backoff | Network timeout, DB locked, 5xx from GitLab |
|
||||
+| Transient (hinted) | Respect server retry hint | Rate limited with Retry-After/X-RateLimit-Reset |
|
||||
|
||||
@@ Backoff Logic
|
||||
+ If an error includes a retry hint (e.g., `Retry-After`), set:
|
||||
+ `next_retry_at_ms = max(computed_backoff, hinted_retry_at_ms)`.
|
||||
+ Persist `backoff_reason` for status visibility.
|
||||
```
|
||||
|
||||
4. **Decouple optional stage cadence from core sync interval**
|
||||
Why this improves the plan: running docs/embeddings every 5–30 minutes is expensive and unnecessary. Separate freshness windows reduce cost/latency while keeping core data fresh.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Sync profiles
|
||||
-| `balanced` (default) | `--no-embed` | Issues + MRs + doc generation |
|
||||
-| `full` | (none) | Full pipeline including embeddings |
|
||||
+| `balanced` (default) | core every interval, docs every 60m, no embeddings | Fast + useful docs |
|
||||
+| `full` | core every interval, docs every interval, embeddings every 6h (default) | Full freshness with bounded cost |
|
||||
|
||||
@@ Status File / StageResult
|
||||
+ /// true when stage intentionally skipped due freshness window
|
||||
+ #[serde(default)]
|
||||
+ pub skipped: bool,
|
||||
|
||||
@@ lore service run / Stage-aware execution
|
||||
+ Optional stages may be skipped when their last successful run is within configured freshness windows.
|
||||
+ Skipped optional stages do not count as failures and are recorded explicitly.
|
||||
```
|
||||
|
||||
5. **Give Windows parity for secure token handling (env-file + wrapper)**
|
||||
Why this improves the plan: current Windows path requires global/system env and has poor UX. A wrapper+env-file model gives platform parity and avoids global token exposure.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Token storage strategies
|
||||
-| On Windows, neither strategy applies — the token must be in the user's system environment
|
||||
+| On Windows, `env-file` is supported via a generated wrapper script (`service-run-{service_id}.cmd` or `.ps1`)
|
||||
+| that reads `{data_dir}/service-env-{service_id}` and launches `lore --robot service run ...`.
|
||||
+| `embedded` remains opt-in and warned as less secure.
|
||||
|
||||
@@ Windows: schtasks
|
||||
- Token handling on Windows: The env var must be set system-wide via `setx`
|
||||
+ Token handling on Windows:
|
||||
+ - `env-file` (default): wrapper script reads token from private file at runtime
|
||||
+ - `embedded`: token passed via wrapper-set environment variable
|
||||
+ - `system_env`: still supported as fallback
|
||||
```
|
||||
|
||||
6. **Add run heartbeat and stale-run detection**
|
||||
Why this improves the plan: `running` state can become misleading after crashes or stale locks. Heartbeat metadata makes status accurate and improves incident triage.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Status File / Schema
|
||||
+ /// In-flight run metadata for crash/stale detection
|
||||
+ #[serde(skip_serializing_if = "Option::is_none")]
|
||||
+ pub current_run: Option<CurrentRunState>,
|
||||
+
|
||||
+pub struct CurrentRunState {
|
||||
+ pub run_id: String,
|
||||
+ pub started_at_ms: i64,
|
||||
+ pub last_heartbeat_ms: i64,
|
||||
+ pub pid: u32,
|
||||
+}
|
||||
|
||||
@@ lore service status
|
||||
- - `running` — currently executing (sync_pipeline lock held)
|
||||
+ - `running` — currently executing with live heartbeat
|
||||
+ - `running_stale` — in-flight metadata exists but heartbeat exceeded stale threshold
|
||||
```
|
||||
|
||||
7. **Upgrade drift detection from “loaded/unloaded” to spec-level drift**
|
||||
Why this improves the plan: platform state alone misses manual edits to unit/plist/wrapper files. Spec-hash drift gives reliable “what changed?” diagnostics and safe regeneration.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ Install Manifest / Schema
|
||||
+ /// Hash of generated scheduler artifacts and command spec
|
||||
+ pub spec_hash: String,
|
||||
|
||||
@@ lore service status
|
||||
- Detects manifest/platform drift and reports it
|
||||
+ Detects:
|
||||
+ - platform drift (loaded/unloaded mismatch)
|
||||
+ - spec drift (artifact content hash mismatch)
|
||||
+ - command drift (sync command differs from manifest)
|
||||
|
||||
@@ lore service repair
|
||||
+ Add `--regenerate` to rewrite scheduler artifacts from manifest when spec drift is detected.
|
||||
+ This is non-destructive and does not delete status/log history.
|
||||
```
|
||||
|
||||
8. **Add safe operational modes: `install --dry-run` and `doctor --fix`**
|
||||
Why this improves the plan: dry-run reduces risk before writing OS scheduler files; fix-mode improves operator ergonomics and lowers support burden.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ lore service install
|
||||
+ Add `--dry-run`:
|
||||
+ - validates config/token/prereqs
|
||||
+ - renders service files and planned commands
|
||||
+ - writes nothing, executes nothing
|
||||
|
||||
@@ lore service doctor
|
||||
+ Add `--fix` for safe, non-destructive remediations:
|
||||
+ - create missing dirs
|
||||
+ - correct file permissions on env/wrapper files
|
||||
+ - run `systemctl --user daemon-reload` when applicable
|
||||
+ - report applied fixes in robot output
|
||||
```
|
||||
|
||||
9. **Define explicit schema migration behavior (not just `schema_version` fields)**
|
||||
Why this improves the plan: version fields without migration policy become operational risk during upgrades.
|
||||
|
||||
```diff
|
||||
--- a/plan.md
|
||||
+++ b/plan.md
|
||||
@@ ServiceManifest Read/Write
|
||||
- `ServiceManifest::read(path: &Path) -> Result<Option<Self>, LoreError>`
|
||||
+ `ServiceManifest::read_and_migrate(path: &Path) -> Result<Option<Self>, LoreError>`
|
||||
+ - Migrates known older schema versions to current in-memory model
|
||||
+ - Rewrites migrated file atomically
|
||||
+ - Fails with actionable `ServiceCorruptState` for unknown future major versions
|
||||
|
||||
@@ SyncStatusFile Read/Write
|
||||
- `SyncStatusFile::read(path: &Path) -> Result<Option<Self>, LoreError>`
|
||||
+ `SyncStatusFile::read_and_migrate(path: &Path) -> Result<Option<Self>, LoreError>`
|
||||
```
|
||||
|
||||
If you want, I can produce a fully rewritten v5 plan text that integrates all nine changes coherently section-by-section.
|
||||
3759
plans/lore-service.md
Normal file
3759
plans/lore-service.md
Normal file
File diff suppressed because it is too large
Load Diff
250
plans/plan-to-beads-v2-draft.md
Normal file
250
plans/plan-to-beads-v2-draft.md
Normal file
@@ -0,0 +1,250 @@
|
||||
# plan-to-beads v2 — Draft for Review
|
||||
|
||||
This is a draft of the improved skill. Review before applying to `~/.claude/skills/plan-to-beads/SKILL.md`.
|
||||
|
||||
---
|
||||
|
||||
```markdown
|
||||
---
|
||||
name: plan-to-beads
|
||||
description: Transforms markdown implementation plans into granular, agent-ready beads with dependency graphs. Each bead is fully self-contained — an agent can execute it with zero external context. Triggers on "break down this plan", "create beads from", "convert to beads", "make issues from plan".
|
||||
argument-hint: "[path/to/plan.md]"
|
||||
---
|
||||
|
||||
# Plan to Beads Conversion
|
||||
|
||||
## The Prime Directive
|
||||
|
||||
**Every bead must be executable by an agent that has ONLY the bead description.** No plan document. No Slack context. No "see the PRD." The bead IS the spec. If an agent can't start coding within 60 seconds of reading the bead, it's not ready.
|
||||
|
||||
## Workflow
|
||||
|
||||
```
|
||||
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
|
||||
│ 1. PARSE │──▶│ 2. MINE │──▶│ 3. BUILD │──▶│ 4. LINK │──▶│ 5. AUDIT │
|
||||
│ Structure│ │ Context │ │ Beads │ │ Deps │ │ Quality │
|
||||
└──────────┘ └──────────┘ └──────────┘ └──────────┘ └──────────┘
|
||||
```
|
||||
|
||||
### 1. Parse Structure
|
||||
|
||||
Read the plan document. Identify:
|
||||
- **Epics**: Major sections / phases / milestones
|
||||
- **Tasks**: Implementable units with clear outcomes (1-4 hour scope)
|
||||
- **Subtasks**: Granular steps within tasks
|
||||
|
||||
### 2. Mine Context
|
||||
|
||||
This is the critical step. For EACH identified task, extract everything an implementing agent will need.
|
||||
|
||||
#### From the plan document:
|
||||
|
||||
| Extract | Where to look | Example |
|
||||
|---------|--------------|---------|
|
||||
| **Rationale** | Intro paragraphs, "why" sections | "We need this because the current approach causes N+1 queries" |
|
||||
| **Approach details** | Implementation notes, code snippets, architecture decisions | "Use a 5-stage pipeline: SEED → HYDRATE → ..." |
|
||||
| **Test requirements** | TDD sections, acceptance criteria, "verify by" notes | "Test that empty input returns empty vec" |
|
||||
| **Edge cases & risks** | Warnings, gotchas, "watch out for" notes | "Multi-byte UTF-8 chars can cause panics at byte boundaries" |
|
||||
| **Data shapes** | Type definitions, struct descriptions, API contracts | "TimelineEvent { kind: EventKind, timestamp: DateTime, ... }" |
|
||||
| **File paths** | Explicit mentions or inferable from module structure | "src/core/timeline_seed.rs" |
|
||||
| **Dependencies on other tasks** | "requires X", "after Y is done", "uses Z from step N" | "Consumes the TimelineEvent struct from the types task" |
|
||||
| **Verification commands** | Test commands, CLI invocations, expected outputs | "cargo test timeline_seed -- --nocapture" |
|
||||
|
||||
#### From the codebase:
|
||||
|
||||
Search the codebase to supplement what the plan says:
|
||||
- Find existing files mentioned or implied by the plan
|
||||
- Discover patterns the task should follow (e.g., how existing similar modules are structured)
|
||||
- Check test files for naming conventions and test infrastructure in use
|
||||
- Confirm exact file paths rather than guessing
|
||||
|
||||
Use codebase search tools (WarpGrep, Explore agent, or targeted Grep/Glob) appropriate to the scope of what you need to find.
|
||||
|
||||
### 3. Build Beads
|
||||
|
||||
Use `br` exclusively.
|
||||
|
||||
| Type | Priority | Command |
|
||||
|------|----------|---------|
|
||||
| Epic | 1 | `br create "Epic: [Title]" -p 1` |
|
||||
| Task | 2-3 | `br create "[Verb] [Object]" -p 2` |
|
||||
| Subtask | 3-4 | `br q "[Verb] [Object]"` |
|
||||
|
||||
**Granularity target**: Each bead completable in 1-4 hours by one agent.
|
||||
|
||||
#### Description Templates
|
||||
|
||||
Use the **full template** for all task-level beads. Use the **light template** only for trivially small tasks (config change, single-line fix, add a re-export).
|
||||
|
||||
##### Full Template (default)
|
||||
|
||||
```
|
||||
## Background
|
||||
[WHY this exists. What problem it solves. How it fits into the larger system.
|
||||
Include enough context that an agent unfamiliar with the project understands
|
||||
the purpose. Reference architectural patterns in use.]
|
||||
|
||||
## Approach
|
||||
[HOW to implement. Be specific:
|
||||
- Data structures / types to create or use (include field names and types)
|
||||
- Algorithms or patterns to follow
|
||||
- Code snippets from the plan if available
|
||||
- Which existing code to reference for patterns (exact file paths)]
|
||||
|
||||
## Acceptance Criteria
|
||||
|
||||
### Specified (from plan — implement as-is)
|
||||
- [ ] <criteria explicitly stated in the plan>
|
||||
- [ ] <criteria explicitly stated in the plan>
|
||||
|
||||
### Proposed (inferred — confirm with user before implementing) [?]
|
||||
- [ ] [?] <criteria the agent inferred but the plan didn't specify>
|
||||
- [ ] [?] <criteria the agent inferred but the plan didn't specify>
|
||||
|
||||
**ASSUMPTION RULE**: If proposed criteria exceed ~30% of total, STOP.
|
||||
The bead needs human input before it's ready for implementation. Flag it
|
||||
in the audit output and ask the user to refine the ACs.
|
||||
|
||||
## Files
|
||||
[Exact paths to create or modify. Confirmed by searching the codebase.]
|
||||
- CREATE: src/foo/bar.rs
|
||||
- MODIFY: src/foo/mod.rs (add pub mod bar)
|
||||
- MODIFY: tests/foo_tests.rs (add test module)
|
||||
|
||||
## TDD Anchor
|
||||
[The first test to write. This grounds the agent's work.]
|
||||
RED: Write `test_<name>` in `<test_file>` that asserts <specific behavior>.
|
||||
GREEN: Implement the minimal code to make it pass.
|
||||
VERIFY: <project's test command> <pattern>
|
||||
|
||||
[If the plan specifies additional tests, list them all:]
|
||||
- test_empty_input_returns_empty_vec
|
||||
- test_single_issue_produces_one_event
|
||||
- test_handles_missing_fields_gracefully
|
||||
|
||||
## Edge Cases
|
||||
[Gotchas, risks, things that aren't obvious. Pulled from the plan's warnings,
|
||||
known issues, or your analysis of the approach.]
|
||||
- <edge case 1>
|
||||
- <edge case 2>
|
||||
|
||||
## Dependency Context
|
||||
[For each dependency, explain WHAT it provides that this bead consumes.
|
||||
Not just "depends on bd-xyz" but "uses the `TimelineEvent` struct and
|
||||
`SeedConfig` type defined in bd-xyz".]
|
||||
```
|
||||
|
||||
##### Light Template (trivially small tasks only)
|
||||
|
||||
Use this ONLY when the task is a one-liner or pure mechanical change (add a re-export, flip a config flag, rename a constant). If there's any ambiguity about approach, use the full template.
|
||||
|
||||
```
|
||||
## What
|
||||
[One sentence: what to do and where.]
|
||||
|
||||
## Acceptance Criteria
|
||||
- [ ] <single binary criterion>
|
||||
|
||||
## Files
|
||||
- MODIFY: <exact path>
|
||||
```
|
||||
|
||||
### 4. Link Dependencies
|
||||
|
||||
```bash
|
||||
br dep add [blocker-id] [blocked-id]
|
||||
```
|
||||
|
||||
Dependency patterns:
|
||||
- Types/structs → code that uses them
|
||||
- Infrastructure (DB, config) → features that need them
|
||||
- Core logic → extensions/enhancements
|
||||
- Tests may depend on test helpers
|
||||
|
||||
**Critical**: When linking deps, update the "Dependency Context" section in the blocked bead to describe exactly what it receives from the blocker.
|
||||
|
||||
### 5. Audit Quality
|
||||
|
||||
Before reporting, review EVERY bead against this checklist:
|
||||
|
||||
| Check | Pass criteria |
|
||||
|-------|--------------|
|
||||
| **Self-contained?** | Agent can start coding in 60 seconds with ONLY this description |
|
||||
| **TDD anchor?** | First test to write is named and described |
|
||||
| **Binary criteria?** | Every acceptance criterion is pass/fail, not subjective |
|
||||
| **Exact paths?** | File paths verified against codebase, not guessed |
|
||||
| **Edge cases?** | At least 1 non-obvious gotcha identified |
|
||||
| **Dep context?** | Each dependency explains WHAT it provides, not just its ID |
|
||||
| **Approach specifics?** | Types, field names, patterns — not "implement the thing" |
|
||||
| **Assumption budget?** | Proposed [?] criteria are <30% of total ACs |
|
||||
|
||||
If a bead fails any check, fix it before moving on. If the assumption budget is exceeded, flag the bead for human review rather than inventing more ACs.
|
||||
|
||||
## Assumption & AC Guidance
|
||||
|
||||
Agents filling in beads will inevitably encounter gaps in the plan. The rules:
|
||||
|
||||
1. **Never silently fill gaps.** If the plan doesn't specify a behavior, don't assume one and bury it in the ACs. Mark it `[?]` so the implementing agent knows to ask.
|
||||
|
||||
2. **Specify provenance on every AC.** Specified = from the plan. Proposed = your inference. The implementing agent treats these differently:
|
||||
- **Specified**: implement without question
|
||||
- **Proposed [?]**: pause and confirm with the user before implementing
|
||||
|
||||
3. **The 30% rule.** If more than ~30% of ACs on a bead are proposed/inferred, the plan was too vague for this task. Don't create the bead as-is. Instead:
|
||||
- Create it with status noting "needs AC refinement"
|
||||
- List the open questions explicitly
|
||||
- Flag it in the output report under "Beads Needing Human Input"
|
||||
|
||||
4. **Prefer smaller scope over more assumptions.** If you're unsure whether a task should handle edge case X, make the bead's scope explicitly exclude it and note it as a potential follow-up. A bead that does less but does it right beats one that guesses wrong.
|
||||
|
||||
5. **Implementing agents: honor the markers.** When you encounter `[?]` on an AC, you MUST ask the user before implementing that behavior. Do not silently resolve it in either direction.
|
||||
|
||||
## Output Format
|
||||
|
||||
After completion, report:
|
||||
|
||||
```
|
||||
## Beads Created: N total (X epics, Y tasks, Z subtasks)
|
||||
|
||||
### Quality Audit
|
||||
- Beads scoring 4+: N/N (target: 100%)
|
||||
- [list any beads that needed extra attention and why]
|
||||
|
||||
### Beads Needing Human Input
|
||||
[List any beads where proposed ACs exceeded 30%, or where significant
|
||||
ambiguity in the plan made self-contained descriptions impossible.
|
||||
Include the specific open questions for each.]
|
||||
|
||||
### Critical Path
|
||||
[blocker] → [blocked] → [blocked]
|
||||
|
||||
### Ready to Start
|
||||
- bd-xxx: [Title] — [one-line summary of what agent will do]
|
||||
- bd-yyy: [Title] — [one-line summary of what agent will do]
|
||||
|
||||
### Dependency Graph
|
||||
[Brief visualization or description of the dep structure]
|
||||
```
|
||||
|
||||
## Risk Tiers
|
||||
|
||||
| Operation | Tier | Behavior |
|
||||
|-----------|------|----------|
|
||||
| `br create` | SAFE | Auto-proceed |
|
||||
| `br dep add` | SAFE | Auto-proceed |
|
||||
| `br update --description` | CAUTION | Verify content |
|
||||
| Bulk creation (>20 beads) | CAUTION | Confirm count first |
|
||||
|
||||
## Anti-Patterns
|
||||
|
||||
| Anti-Pattern | Why it's bad | Fix |
|
||||
|-------------|-------------|-----|
|
||||
| "Implement the pipeline stage" | Agent doesn't know WHAT to implement | Name the types, the function signatures, the test |
|
||||
| "See plan for details" | Plan isn't available to the agent | Copy the relevant details INTO the bead |
|
||||
| "Files: probably src/foo/" | Agent wastes time finding the right file | Search the codebase, confirm exact paths |
|
||||
| "Should work correctly" | Not binary, not testable | "test_x passes" or "output matches Y" |
|
||||
| No TDD anchor | Agent doesn't know where to start | Always specify the first test to write |
|
||||
| "Depends on bd-xyz" (without context) | Agent doesn't know what bd-xyz provides | "Uses FooStruct and bar() function from bd-xyz" |
|
||||
| Single-line description | Score 1 bead, agent is stuck | Use the full template, every section |
|
||||
| Silently invented ACs | User surprised by implementation choices | Mark inferred ACs with [?], honor the 30% rule |
|
||||
```
|
||||
114
plans/time-decay-expert-scoring.feedback-1.md
Normal file
114
plans/time-decay-expert-scoring.feedback-1.md
Normal file
@@ -0,0 +1,114 @@
|
||||
Your plan is strong directionally, but I’d revise it in 8 key places to avoid regressions and make it significantly more useful in production.
|
||||
|
||||
1. **Split reviewer signals into “participated” vs “assigned-only”**
|
||||
Reason: today’s inflation problem is often assignment noise. Treating `mr_reviewers` equal to real review activity still over-ranks passive reviewers.
|
||||
|
||||
```diff
|
||||
@@ Per-signal contributions
|
||||
-| Reviewer (reviewed MR touching path) | 10 | 90 days |
|
||||
+| ReviewerParticipated (left DiffNote on MR/path) | 10 | 90 days |
|
||||
+| ReviewerAssignedOnly (in mr_reviewers, no DiffNote by that user on MR/path) | 3 | 45 days |
|
||||
```
|
||||
|
||||
```diff
|
||||
@@ Scoring Formula
|
||||
-score = reviewer_mr * reviewer_weight + ...
|
||||
+score = reviewer_participated * reviewer_weight
|
||||
+ + reviewer_assigned_only * reviewer_assignment_weight
|
||||
+ + ...
|
||||
```
|
||||
|
||||
2. **Cap/saturate note intensity per MR**
|
||||
Reason: raw per-note addition can still reward “comment storms.” Use diminishing returns.
|
||||
|
||||
```diff
|
||||
@@ Rust-Side Aggregation
|
||||
-- Notes: Vec<i64> (timestamps) from diffnote_reviewer
|
||||
+-- Notes grouped per (username, mr_id): note_count + max_ts
|
||||
+-- Note contribution per MR uses diminishing returns:
|
||||
+-- note_score_mr = note_bonus * ln(1 + note_count) * decay(now - ts, note_hl)
|
||||
```
|
||||
|
||||
3. **Use better event timestamps than `m.updated_at` for file-change signals**
|
||||
Reason: `updated_at` is noisy (title edits, metadata touches) and creates false recency.
|
||||
|
||||
```diff
|
||||
@@ SQL Restructure
|
||||
- signal 3/4 seen_at = m.updated_at
|
||||
+ signal 3/4 activity_ts = COALESCE(m.merged_at, m.closed_at, m.created_at, m.updated_at)
|
||||
```
|
||||
|
||||
4. **Don’t stream raw note rows to Rust; pre-aggregate in SQL**
|
||||
Reason: current plan removes SQL grouping and can blow up memory/latency on large repos.
|
||||
|
||||
```diff
|
||||
@@ SQL Restructure
|
||||
-SELECT username, signal, mr_id, note_id, ts FROM signals
|
||||
+WITH raw_signals AS (...),
|
||||
+aggregated AS (
|
||||
+ -- 1 row per (username, signal_class, mr_id) for MR-level signals
|
||||
+ -- 1 row per (username, mr_id) for note_count + max_ts
|
||||
+)
|
||||
+SELECT username, signal_class, mr_id, qty, ts FROM aggregated
|
||||
```
|
||||
|
||||
5. **Replace fixed `"24m"` with model-driven cutoff**
|
||||
Reason: hardcoded 24m is arbitrary and tied to current weights/half-lives only.
|
||||
|
||||
```diff
|
||||
@@ Default --since Change
|
||||
-Expert mode: "6m" -> "24m"
|
||||
+Expert mode default window derived from scoring.max_age_days (default 1095 days / 36m).
|
||||
+Formula guidance: choose max_age where max possible single-event contribution < epsilon (e.g. 0.25 points).
|
||||
+Add `--all-history` to disable cutoff for diagnostics.
|
||||
```
|
||||
|
||||
6. **Validate scoring config explicitly**
|
||||
Reason: silent bad configs (`half_life_days = 0`, negative weights) create undefined behavior.
|
||||
|
||||
```diff
|
||||
@@ ScoringConfig (config.rs)
|
||||
pub struct ScoringConfig {
|
||||
pub author_weight: i64,
|
||||
pub reviewer_weight: i64,
|
||||
pub note_bonus: i64,
|
||||
+ pub reviewer_assignment_weight: i64, // default: 3
|
||||
pub author_half_life_days: u32,
|
||||
pub reviewer_half_life_days: u32,
|
||||
pub note_half_life_days: u32,
|
||||
+ pub reviewer_assignment_half_life_days: u32, // default: 45
|
||||
+ pub max_age_days: u32, // default: 1095
|
||||
}
|
||||
@@ Config::load_from_path
|
||||
+validate_scoring(&config.scoring)?; // weights >= 0, half_life_days > 0, max_age_days >= 30
|
||||
```
|
||||
|
||||
7. **Keep raw float score internally; round only for display**
|
||||
Reason: rounding before sort causes avoidable ties/rank instability.
|
||||
|
||||
```diff
|
||||
@@ Rust-Side Aggregation
|
||||
-Round to i64 for Expert.score field
|
||||
+Compute `raw_score: f64`, sort by raw_score DESC.
|
||||
+Expose integer `score` for existing UX.
|
||||
+Optionally expose `score_raw` and `score_components` in robot JSON when `--explain-score`.
|
||||
```
|
||||
|
||||
8. **Add confidence + data-completeness metadata**
|
||||
Reason: rankings are misleading if `mr_file_changes` coverage is poor.
|
||||
|
||||
```diff
|
||||
@@ ExpertResult / Output
|
||||
+confidence: "high" | "medium" | "low"
|
||||
+coverage: { mrs_with_file_changes, total_mrs_in_window, percent }
|
||||
+warning when coverage < threshold (e.g. 70%)
|
||||
```
|
||||
|
||||
```diff
|
||||
@@ Verification
|
||||
4. cargo test
|
||||
+5. ubs src/cli/commands/who.rs src/core/config.rs
|
||||
+6. Benchmark query_expert on representative DB (latency + rows scanned before/after)
|
||||
```
|
||||
|
||||
If you want, I can rewrite your full plan document into a clean “v2” version that already incorporates these diffs end-to-end.
|
||||
132
plans/time-decay-expert-scoring.feedback-2.md
Normal file
132
plans/time-decay-expert-scoring.feedback-2.md
Normal file
@@ -0,0 +1,132 @@
|
||||
The plan is strong, but I’d revise it in 10 places to improve correctness, scalability, and operator trust.
|
||||
|
||||
1. **Add rename/old-path awareness (correctness gap)**
|
||||
Analysis: right now both existing code and your plan still center on `position_new_path` / `new_path` matches (`src/cli/commands/who.rs:643`, `src/cli/commands/who.rs:681`). That misses expertise on renamed/deleted paths and under-ranks long-time owners after refactors.
|
||||
|
||||
```diff
|
||||
@@ ## Context
|
||||
-This produces two compounding problems:
|
||||
+This produces three compounding problems:
|
||||
@@
|
||||
2. **Reviewer inflation**: ...
|
||||
+3. **Path-history blindness**: Renamed/moved files lose historical expertise because matching relies on current-path fields only.
|
||||
|
||||
@@ ### 3. SQL Restructure (who.rs)
|
||||
-AND n.position_new_path {path_op}
|
||||
+AND (n.position_new_path {path_op} OR n.position_old_path {path_op})
|
||||
|
||||
-AND fc.new_path {path_op}
|
||||
+AND (fc.new_path {path_op} OR fc.old_path {path_op})
|
||||
```
|
||||
|
||||
2. **Follow rename chains for queried paths**
|
||||
Analysis: matching `old_path` helps, but true continuity needs alias expansion (A→B→C). Without this, expertise before multi-hop renames is fragmented.
|
||||
|
||||
```diff
|
||||
@@ ### 3. SQL Restructure (who.rs)
|
||||
+**Path alias expansion**: Before scoring, resolve a bounded rename alias set (default max depth: 20)
|
||||
+from `mr_file_changes(change_type='renamed')`. Query signals against all aliases.
|
||||
+Output includes `path_aliases_used` for transparency.
|
||||
```
|
||||
|
||||
3. **Use hybrid SQL pre-aggregation instead of fully raw rows**
|
||||
Analysis: the “raw row” design is simpler but will degrade on large repos with heavy DiffNote volume. Pre-aggregating to `(user, mr)` for MR signals and `(user, mr, note_count)` for note signals keeps memory/latency predictable.
|
||||
|
||||
```diff
|
||||
@@ ### 3. SQL Restructure (who.rs)
|
||||
-The SQL CTE ... removes the outer GROUP BY aggregation. Instead, it returns raw signal rows:
|
||||
-SELECT username, signal, mr_id, note_id, ts FROM signals
|
||||
+Use hybrid aggregation:
|
||||
+- SQL returns MR-level rows for author/reviewer signals (1 row per user+MR+signal_class)
|
||||
+- SQL returns note groups (1 row per user+MR with note_count, max_ts)
|
||||
+- Rust applies decay + ln(1+count) + final ranking.
|
||||
```
|
||||
|
||||
4. **Make timestamp policy state-aware (merged vs opened)**
|
||||
Analysis: replacing `updated_at` with only `COALESCE(merged_at, created_at)` over-decays long-running open MRs. Open MRs need recency from active lifecycle; merged MRs should anchor to merge time.
|
||||
|
||||
```diff
|
||||
@@ ### 3. SQL Restructure (who.rs)
|
||||
-Replace m.updated_at with COALESCE(m.merged_at, m.created_at)
|
||||
+Use state-aware timestamp:
|
||||
+activity_ts =
|
||||
+ CASE
|
||||
+ WHEN m.state = 'merged' THEN COALESCE(m.merged_at, m.updated_at, m.created_at, m.last_seen_at)
|
||||
+ WHEN m.state = 'opened' THEN COALESCE(m.updated_at, m.created_at, m.last_seen_at)
|
||||
+ END
|
||||
```
|
||||
|
||||
5. **Replace fixed `24m` with config-driven max age**
|
||||
Analysis: `24m` is reasonable now, but brittle after tuning weights/half-lives. Tie cutoff to config so model behavior remains coherent as parameters evolve.
|
||||
|
||||
```diff
|
||||
@@ ### 1. ScoringConfig (config.rs)
|
||||
+pub max_age_days: u32, // default: 730 (or 1095)
|
||||
|
||||
@@ ### 5. Default --since Change
|
||||
-Expert mode: "6m" -> "24m"
|
||||
+Expert mode default window derives from `scoring.max_age_days`
|
||||
+unless user passes `--since` or `--all-history`.
|
||||
```
|
||||
|
||||
6. **Add reproducible scoring time via `--as-of`**
|
||||
Analysis: decayed ranking is time-sensitive; debugging and tests become flaky without a fixed reference clock. This improves reliability and incident triage.
|
||||
|
||||
```diff
|
||||
@@ ## Files to Modify
|
||||
-2. src/cli/commands/who.rs
|
||||
+2. src/cli/commands/who.rs
|
||||
+3. src/cli/mod.rs
|
||||
+4. src/main.rs
|
||||
|
||||
@@ ### 5. Default --since Change
|
||||
+Add `--as-of <RFC3339|YYYY-MM-DD>` to score at a fixed timestamp.
|
||||
+`resolved_input` includes `as_of_ms` and `as_of_iso`.
|
||||
```
|
||||
|
||||
7. **Add explainability output (`--explain-score`)**
|
||||
Analysis: decayed multi-signal ranking will be disputed without decomposition. Show components and top evidence MRs to make results actionable and debuggable.
|
||||
|
||||
```diff
|
||||
@@ ## Rejected Ideas (with rationale)
|
||||
-- **`--explain-score` flag with component breakdown**: ... deferred
|
||||
+**Included in this iteration**: `--explain-score` adds per-user score components
|
||||
+(`author`, `review_participated`, `review_assigned`, `notes`) plus top evidence MRs.
|
||||
```
|
||||
|
||||
8. **Add confidence/coverage metadata**
|
||||
Analysis: rankings can look precise while data is incomplete (`mr_file_changes` gaps, sparse DiffNotes). Confidence fields prevent false certainty.
|
||||
|
||||
```diff
|
||||
@@ ### 4. Rust-Side Aggregation (who.rs)
|
||||
+Compute and emit:
|
||||
+- `coverage`: {mrs_considered, mrs_with_file_changes, mrs_with_diffnotes, percent}
|
||||
+- `confidence`: high|medium|low (threshold-based)
|
||||
```
|
||||
|
||||
9. **Add index migration for new query shapes**
|
||||
Analysis: your new `EXISTS/NOT EXISTS` reviewer split and path dual-matching will need better indexes; current `who` indexes are not enough for author+path+time combinations.
|
||||
|
||||
```diff
|
||||
@@ ## Files to Modify
|
||||
+3. **`migrations/021_who_decay_indexes.sql`** — indexes for decay query patterns:
|
||||
+ - notes(diffnote path + author + created_at + discussion_id) partial
|
||||
+ - notes(old_path variant) partial
|
||||
+ - mr_file_changes(project_id, new_path, merge_request_id)
|
||||
+ - mr_file_changes(project_id, old_path, merge_request_id) partial
|
||||
+ - merge_requests(state, merged_at, updated_at, created_at)
|
||||
```
|
||||
|
||||
10. **Expand tests to invariants and determinism**
|
||||
Analysis: example-based tests are good, but ranking systems need invariant tests to avoid subtle regressions.
|
||||
|
||||
```diff
|
||||
@@ ### 7. New Tests (TDD)
|
||||
+**`test_score_monotonicity_by_age`**: same signal, older timestamp never scores higher
|
||||
+**`test_row_order_independence`**: shuffled SQL row order yields identical ranking
|
||||
+**`test_as_of_reproducibility`**: same data + same `--as-of` => identical output
|
||||
+**`test_rename_alias_chain_scoring`**: expertise carries across A->B->C rename chain
|
||||
+**`test_overlap_participated_vs_assigned_counts`**: overlap reflects split reviewer semantics
|
||||
```
|
||||
|
||||
If you want, I can produce a full consolidated `v2` plan doc patch (single unified diff against `plans/time-decay-expert-scoring.md`) rather than per-change snippets.
|
||||
167
plans/time-decay-expert-scoring.feedback-3.md
Normal file
167
plans/time-decay-expert-scoring.feedback-3.md
Normal file
@@ -0,0 +1,167 @@
|
||||
**Critical Plan Findings First**
|
||||
1. The proposed index `idx_notes_mr_path_author ON notes(noteable_id, ...)` will fail: `notes.noteable_id` does not exist in schema (`migrations/002_issues.sql:74`).
|
||||
2. Rename awareness is only applied in scoring queries, not in path resolution probes; today `build_path_query()` and `suffix_probe()` only inspect `position_new_path`/`new_path` (`src/cli/commands/who.rs:465`, `src/cli/commands/who.rs:591`), so old-path queries can still miss.
|
||||
3. A fixed `"24m"` default window is brittle once half-lives become configurable; it can silently truncate meaningful history for larger half-lives.
|
||||
|
||||
Below are the revisions I’d make to your plan.
|
||||
|
||||
1. **Fix migration/index architecture (blocking correctness + perf)**
|
||||
Rationale: prevents migration failure and aligns indexes to actual query shapes.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ ### 6. Index Migration (db.rs)
|
||||
- -- Support EXISTS subquery for reviewer participation check
|
||||
- CREATE INDEX IF NOT EXISTS idx_notes_mr_path_author
|
||||
- ON notes(noteable_id, position_new_path, author_username)
|
||||
- WHERE note_type = 'DiffNote' AND is_system = 0;
|
||||
+ -- Support reviewer participation joins (notes -> discussions -> MR)
|
||||
+ CREATE INDEX IF NOT EXISTS idx_notes_diffnote_discussion_author_created
|
||||
+ ON notes(discussion_id, author_username, created_at)
|
||||
+ WHERE note_type = 'DiffNote' AND is_system = 0;
|
||||
+
|
||||
+ -- Path-first indexes for global and project-scoped path lookups
|
||||
+ CREATE INDEX IF NOT EXISTS idx_mfc_new_path_project_mr
|
||||
+ ON mr_file_changes(new_path, project_id, merge_request_id);
|
||||
+ CREATE INDEX IF NOT EXISTS idx_mfc_old_path_project_mr
|
||||
+ ON mr_file_changes(old_path, project_id, merge_request_id)
|
||||
+ WHERE old_path IS NOT NULL;
|
||||
@@
|
||||
- -- Support state-aware timestamp selection
|
||||
- CREATE INDEX IF NOT EXISTS idx_mr_state_timestamps
|
||||
- ON merge_requests(state, merged_at, closed_at, updated_at, created_at);
|
||||
+ -- Removed: low-selectivity timestamp composite index; joins are MR-id driven.
|
||||
```
|
||||
|
||||
2. **Restructure SQL around `matched_mrs` CTE instead of repeating OR path clauses**
|
||||
Rationale: better index use, less duplicated logic, cleaner maintenance.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ ### 3. SQL Restructure (who.rs)
|
||||
- WITH raw AS (
|
||||
- -- 5 UNION ALL subqueries (signals 1, 2, 3, 4a, 4b)
|
||||
- ),
|
||||
+ WITH matched_notes AS (
|
||||
+ -- DiffNotes matching new_path
|
||||
+ ...
|
||||
+ UNION ALL
|
||||
+ -- DiffNotes matching old_path
|
||||
+ ...
|
||||
+ ),
|
||||
+ matched_file_changes AS (
|
||||
+ -- file changes matching new_path
|
||||
+ ...
|
||||
+ UNION ALL
|
||||
+ -- file changes matching old_path
|
||||
+ ...
|
||||
+ ),
|
||||
+ matched_mrs AS (
|
||||
+ SELECT DISTINCT mr_id, project_id FROM matched_notes
|
||||
+ UNION
|
||||
+ SELECT DISTINCT mr_id, project_id FROM matched_file_changes
|
||||
+ ),
|
||||
+ raw AS (
|
||||
+ -- signals sourced from matched_mrs + matched_notes
|
||||
+ ),
|
||||
```
|
||||
|
||||
3. **Replace correlated `EXISTS/NOT EXISTS` reviewer split with one precomputed participation set**
|
||||
Rationale: same semantics, lower query cost, easier reasoning.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ Signal 4 splits into two
|
||||
- Signal 4a uses an EXISTS subquery ...
|
||||
- Signal 4b uses NOT EXISTS ...
|
||||
+ Build `reviewer_participation(mr_id, username)` once from matched DiffNotes.
|
||||
+ Then classify `mr_reviewers` rows via LEFT JOIN:
|
||||
+ - participated: `rp.username IS NOT NULL`
|
||||
+ - assigned-only: `rp.username IS NULL`
|
||||
+ This avoids correlated EXISTS scans per reviewer row.
|
||||
```
|
||||
|
||||
4. **Make default `--since` derived from half-life + decay floor, not hardcoded 24m**
|
||||
Rationale: remains mathematically consistent when config changes.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ ### 1. ScoringConfig (config.rs)
|
||||
+ pub decay_floor: f64, // default: 0.05
|
||||
@@ ### 5. Default --since Change
|
||||
- Expert mode: "6m" -> "24m"
|
||||
+ Expert mode default window is computed:
|
||||
+ default_since_days = ceil(max_half_life_days * log2(1.0 / decay_floor))
|
||||
+ With defaults (max_half_life=180, floor=0.05), this is ~26 months.
|
||||
+ CLI `--since` still overrides; `--all-history` still disables windowing.
|
||||
```
|
||||
|
||||
5. **Use `log2(1+count)` for notes instead of `ln(1+count)`**
|
||||
Rationale: keeps 1 note ~= 1 unit (with `note_bonus=1`) while preserving diminishing returns.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ Scoring Formula
|
||||
- note_contribution(mr) = note_bonus * ln(1 + note_count_in_mr) * 2^(-days_elapsed / note_half_life)
|
||||
+ note_contribution(mr) = note_bonus * log2(1 + note_count_in_mr) * 2^(-days_elapsed / note_half_life)
|
||||
```
|
||||
|
||||
6. **Guarantee deterministic float aggregation and expose `score_raw`**
|
||||
Rationale: avoids hash-order drift and explainability mismatch vs rounded integer score.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ ### 4. Rust-Side Aggregation (who.rs)
|
||||
- HashMap<i64, ...>
|
||||
+ BTreeMap<i64, ...> (or sort keys before accumulation) for deterministic summation order
|
||||
+ Use compensated summation (Kahan/Neumaier) for stable f64 totals
|
||||
@@
|
||||
- Sort on raw `f64` score ... round only for display
|
||||
+ Keep `score_raw` internally and expose when `--explain-score` is active.
|
||||
+ `score` remains integer for backward compatibility.
|
||||
```
|
||||
|
||||
7. **Extend rename awareness to query resolution (not only scoring)**
|
||||
Rationale: fixes user-facing misses for old path input and suffix lookup.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ Path rename awareness
|
||||
- All signal subqueries match both old and new path columns
|
||||
+ Also update `build_path_query()` probes and suffix probe:
|
||||
+ - exact_exists: new_path OR old_path (notes + mr_file_changes)
|
||||
+ - prefix_exists: new_path LIKE OR old_path LIKE
|
||||
+ - suffix_probe: union of notes.position_new_path, notes.position_old_path,
|
||||
+ mr_file_changes.new_path, mr_file_changes.old_path
|
||||
```
|
||||
|
||||
8. **Tighten CLI/output contracts for new flags**
|
||||
Rationale: avoids payload bloat/ambiguity and keeps robot clients stable.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ ### 5b. Score Explainability via `--explain-score`
|
||||
+ `--explain-score` conflicts with `--detail` (mutually exclusive)
|
||||
+ `resolved_input` includes `as_of_ms`, `as_of_iso`, `scoring_model_version`
|
||||
+ robot output includes `score_raw` and `components` only when explain is enabled
|
||||
```
|
||||
|
||||
9. **Add confidence metadata (promote from rejected to accepted)**
|
||||
Rationale: makes ranking more actionable and trustworthy with sparse evidence.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ Rejected Ideas (with rationale)
|
||||
- Confidence/coverage metadata: ... Deferred to avoid scope creep
|
||||
+ Confidence/coverage metadata: ACCEPTED (minimal v1)
|
||||
+ Add per-user `confidence: low|medium|high` based on evidence breadth + recency.
|
||||
+ Keep implementation lightweight (no extra SQL pass).
|
||||
```
|
||||
|
||||
10. **Upgrade test and verification scope to include query-plan and clock semantics**
|
||||
Rationale: catches regressions your current tests won’t.
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ 8. New Tests (TDD)
|
||||
+ test_old_path_probe_exact_and_prefix
|
||||
+ test_suffix_probe_uses_old_path_sources
|
||||
+ test_since_relative_to_as_of_clock
|
||||
+ test_explain_and_detail_are_mutually_exclusive
|
||||
+ test_null_timestamp_fallback_to_created_at
|
||||
+ test_query_plan_uses_path_indexes (EXPLAIN QUERY PLAN)
|
||||
@@ Verification
|
||||
+ 7. EXPLAIN QUERY PLAN snapshots for expert query (exact + prefix) confirm index usage
|
||||
```
|
||||
|
||||
If you want, I can produce a single consolidated “revision 3” plan document that fully merges all of the above into your original structure.
|
||||
133
plans/time-decay-expert-scoring.feedback-4.md
Normal file
133
plans/time-decay-expert-scoring.feedback-4.md
Normal file
@@ -0,0 +1,133 @@
|
||||
Your plan is already strong. The biggest remaining gaps are temporal correctness, indexability at scale, and ranking reliability under sparse/noisy evidence. These are the revisions I’d make.
|
||||
|
||||
1. **Fix temporal correctness for `--as-of` (critical)**
|
||||
Analysis: Right now the plan describes `--as-of`, but the SQL only enforces lower bounds (`>= since`). If `as_of` is in the past, “future” events can still enter and get full weight (because elapsed is clamped). This breaks reproducibility.
|
||||
```diff
|
||||
@@ 3. SQL Restructure
|
||||
- AND n.created_at >= ?2
|
||||
+ AND n.created_at BETWEEN ?2 AND ?4
|
||||
@@ Signal 3/4a/4b
|
||||
- AND {state_aware_ts} >= ?2
|
||||
+ AND {state_aware_ts} BETWEEN ?2 AND ?4
|
||||
|
||||
@@ 5a. Reproducible Scoring via --as-of
|
||||
- All decay computations use as_of_ms instead of SystemTime::now()
|
||||
+ All event selection and decay computations are bounded by as_of_ms.
|
||||
+ Query window is [since_ms, as_of_ms], never [since_ms, now_ms].
|
||||
+ Add test: test_as_of_excludes_future_events.
|
||||
```
|
||||
|
||||
2. **Resolve `closed`-state inconsistency**
|
||||
Analysis: The CASE handles `closed`, but all signal queries filter to `('opened','merged')`, making the `closed_at` branch dead code. Either include closed MRs intentionally or remove that logic. I’d include closed with a reduced multiplier.
|
||||
```diff
|
||||
@@ ScoringConfig (config.rs)
|
||||
+ pub closed_mr_multiplier: f64, // default: 0.5
|
||||
|
||||
@@ 3. SQL Restructure
|
||||
- AND m.state IN ('opened','merged')
|
||||
+ AND m.state IN ('opened','merged','closed')
|
||||
|
||||
@@ 4. Rust-Side Aggregation
|
||||
+ if state == "closed" { contribution *= closed_mr_multiplier; }
|
||||
```
|
||||
|
||||
3. **Replace `OR` path predicates with index-friendly `UNION ALL` branches**
|
||||
Analysis: `(new_path ... OR old_path ...)` often degrades index usage in SQLite. Split into two indexed branches and dedupe once. This improves planner stability and latency on large datasets.
|
||||
```diff
|
||||
@@ 3. SQL Restructure
|
||||
-WITH matched_notes AS (
|
||||
- ... AND (n.position_new_path {path_op} OR n.position_old_path {path_op})
|
||||
-),
|
||||
+WITH matched_notes AS (
|
||||
+ SELECT ... FROM notes n WHERE ... AND n.position_new_path {path_op}
|
||||
+ UNION ALL
|
||||
+ SELECT ... FROM notes n WHERE ... AND n.position_old_path {path_op}
|
||||
+),
|
||||
+matched_notes_dedup AS (
|
||||
+ SELECT DISTINCT id, discussion_id, author_username, created_at, project_id
|
||||
+ FROM matched_notes
|
||||
+),
|
||||
@@
|
||||
- JOIN matched_notes mn ...
|
||||
+ JOIN matched_notes_dedup mn ...
|
||||
```
|
||||
|
||||
4. **Add canonical path identity (rename-chain support)**
|
||||
Analysis: Direct `old_path/new_path` matching only handles one-hop rename scenarios. A small alias graph/table built at ingest time gives robust expertise continuity across A→B→C chains and avoids repeated SQL complexity.
|
||||
```diff
|
||||
@@ Files to Modify
|
||||
- 3. src/core/db.rs — Add migration for indexes...
|
||||
+ 3. src/core/db.rs — Add migration for indexes + path_identity table
|
||||
+ 4. src/core/ingest/*.rs — populate path_identity on rename events
|
||||
+ 5. src/cli/commands/who.rs — resolve query path to canonical path_id first
|
||||
|
||||
@@ Context
|
||||
- The fix has three parts:
|
||||
+ The fix has four parts:
|
||||
+ - Introduce canonical path identity so multi-hop renames preserve expertise
|
||||
```
|
||||
|
||||
5. **Split scoring engine into a versioned core module**
|
||||
Analysis: `who.rs` is becoming a mixed CLI/query/math/output surface. Move scoring math and event normalization into `src/core/scoring/` with explicit model versions. This reduces regression risk and enables future model experiments.
|
||||
```diff
|
||||
@@ Files to Modify
|
||||
+ 4. src/core/scoring/mod.rs — model interface + shared types
|
||||
+ 5. src/core/scoring/model_v2_decay.rs — current implementation
|
||||
+ 6. src/cli/commands/who.rs — orchestration only
|
||||
|
||||
@@ 5b. Score Explainability
|
||||
+ resolved_input includes scoring_model_version and scoring_model_name
|
||||
```
|
||||
|
||||
6. **Add evidence confidence to reduce sparse-data rank spikes**
|
||||
Analysis: One recent MR can outrank broader, steadier expertise. Add a confidence factor derived from number of distinct evidence MRs and expose both `score_raw` and `score_adjusted`.
|
||||
```diff
|
||||
@@ Scoring Formula
|
||||
+ confidence(user) = 1 - exp(-evidence_mr_count / 6.0)
|
||||
+ score_adjusted = score_raw * confidence
|
||||
|
||||
@@ 4. Rust-Side Aggregation
|
||||
+ compute evidence_mr_count from unique MR ids across all signals
|
||||
+ sort by score_adjusted DESC, then score_raw DESC, then last_seen DESC
|
||||
|
||||
@@ 5b. --explain-score
|
||||
+ include confidence and evidence_mr_count
|
||||
```
|
||||
|
||||
7. **Add first-class bot/service-account filtering**
|
||||
Analysis: Reviewer inflation is not just assignment; bots and automation users can still pollute rankings. Make exclusion explicit and configurable.
|
||||
```diff
|
||||
@@ ScoringConfig (config.rs)
|
||||
+ pub excluded_username_patterns: Vec<String>, // defaults include "*bot*", "renovate", "dependabot"
|
||||
|
||||
@@ 3. SQL Restructure
|
||||
+ AND username NOT MATCHES excluded patterns (applied in Rust post-query or SQL where feasible)
|
||||
|
||||
@@ CLI
|
||||
+ --include-bots (override exclusions)
|
||||
```
|
||||
|
||||
8. **Tighten reviewer “participated” with substantive-note threshold**
|
||||
Analysis: A single “LGTM” note shouldn’t classify someone as engaged reviewer equivalent to real inline review. Use a minimum substantive threshold.
|
||||
```diff
|
||||
@@ ScoringConfig (config.rs)
|
||||
+ pub reviewer_min_note_chars: u32, // default: 20
|
||||
|
||||
@@ reviewer_participation CTE
|
||||
- SELECT DISTINCT ... FROM matched_notes
|
||||
+ SELECT DISTINCT ... FROM matched_notes
|
||||
+ WHERE LENGTH(TRIM(body)) >= ?reviewer_min_note_chars
|
||||
```
|
||||
|
||||
9. **Add rollout safety: model compare mode + rank-delta diagnostics**
|
||||
Analysis: This is a scoring-model migration. You need safe rollout mechanics, not just tests. Add a compare mode so you can inspect rank deltas before forcing v2.
|
||||
```diff
|
||||
@@ CLI (who)
|
||||
+ --scoring-model v1|v2|compare (default: v2)
|
||||
+ --max-rank-delta-report N (compare mode diagnostics)
|
||||
|
||||
@@ Robot output
|
||||
+ include v1_score, v2_score, rank_delta when --scoring-model compare
|
||||
```
|
||||
|
||||
If you want, I can produce a single consolidated “plan v4” document that applies all nine diffs cleanly into your original markdown.
|
||||
128
plans/time-decay-expert-scoring.feedback-5.md
Normal file
128
plans/time-decay-expert-scoring.feedback-5.md
Normal file
@@ -0,0 +1,128 @@
|
||||
**Best Revisions To Strengthen The Plan**
|
||||
|
||||
1. **[Critical] Replace one-hop rename matching with canonical path identities**
|
||||
Analysis and rationale: Current `old_path OR new_path` fixes direct renames, but it still breaks on rename chains (`a.rs -> b.rs -> c.rs`) and split/move patterns. A canonical `path_identity` graph built from `mr_file_changes(old_path,new_path)` gives stable identity over time, which is the right architectural boundary for expertise history.
|
||||
```diff
|
||||
@@ ## Context
|
||||
-- Match both old and new paths in all signal queries AND path resolution probes so expertise survives file renames
|
||||
+- Build canonical path identities from rename edges and score by identity, not raw path strings, so expertise survives multi-hop renames and moves
|
||||
|
||||
@@ ## Files to Modify
|
||||
-2. **`src/cli/commands/who.rs`** — Core changes:
|
||||
+2. **`src/cli/commands/who.rs`** — Core changes:
|
||||
...
|
||||
- - Match both `new_path` and `old_path` in all signal queries (rename awareness)
|
||||
+ - Resolve queried paths to `path_identity_id` and match all aliases in that identity set
|
||||
+4. **`src/core/path_identity.rs`** — New module:
|
||||
+ - Build/maintain rename graph from `mr_file_changes`
|
||||
+ - Resolve path -> identity + aliases for probes/scoring
|
||||
```
|
||||
|
||||
2. **[Critical] Shift scoring input from runtime CTE joins to a normalized `expertise_events` table**
|
||||
Analysis and rationale: Your SQL is correct but complex and expensive at query time. Precomputing normalized events at ingestion gives simpler, faster, and more reliable scoring queries; it also enables model versioning/backfills without touching raw MR/note tables each request.
|
||||
```diff
|
||||
@@ ## Files to Modify
|
||||
-3. **`src/core/db.rs`** — Add migration for indexes supporting the new query shapes
|
||||
+3. **`src/core/db.rs`** — Add migrations for:
|
||||
+ - `expertise_events` table (normalized scoring events)
|
||||
+ - supporting indexes
|
||||
+4. **`src/core/ingest/expertise_events.rs`** — New:
|
||||
+ - Incremental upsert of events during sync/ingest
|
||||
|
||||
@@ ## SQL Restructure (who.rs)
|
||||
-The SQL uses CTE-based dual-path matching and hybrid aggregation...
|
||||
+Runtime SQL reads precomputed `expertise_events` filtered by path identity + time window.
|
||||
+Heavy joins/aggregation move to ingest-time normalization.
|
||||
```
|
||||
|
||||
3. **[High] Upgrade reviewer engagement model beyond char-count threshold**
|
||||
Analysis and rationale: `min_note_chars` is a useful guardrail but brittle (easy to game, penalizes concise high-quality comments). Add explicit review-state signals (`approved`, `changes_requested`) and trivial-comment pattern filtering to better capture real reviewer expertise.
|
||||
```diff
|
||||
@@ ## Scoring Formula
|
||||
-| **Reviewer Participated** (left DiffNote on MR/path) | 10 | 90 days |
|
||||
+| **Reviewer Participated** (substantive DiffNote and/or formal review action) | 10 | 90 days |
|
||||
+| **Review Decision: changes_requested** | 6 | 120 days |
|
||||
+| **Review Decision: approved** | 4 | 75 days |
|
||||
|
||||
@@ ### 1. ScoringConfig (config.rs)
|
||||
pub reviewer_min_note_chars: u32,
|
||||
+ pub reviewer_trivial_note_patterns: Vec<String>, // default: ["lgtm","+1","nit","ship it","👍"]
|
||||
+ pub review_approved_weight: i64, // default: 4
|
||||
+ pub review_changes_requested_weight: i64, // default: 6
|
||||
```
|
||||
|
||||
4. **[High] Make temporal semantics explicit and deterministic**
|
||||
Analysis and rationale: `--as-of` is good, but day parsing and boundary semantics can still cause subtle reproducibility issues. Define window as `[since_ms, as_of_ms)` and parse `YYYY-MM-DD` as end-of-day UTC (or explicit timezone) so user expectations match outputs.
|
||||
```diff
|
||||
@@ ### 5a. Reproducible Scoring via `--as-of`
|
||||
-- All event selection is bounded by `[since_ms, as_of_ms]`
|
||||
+- All event selection is bounded by `[since_ms, as_of_ms)` (exclusive upper bound)
|
||||
+- `YYYY-MM-DD` is interpreted as `23:59:59.999Z` unless `--timezone` is provided
|
||||
+- Robot output includes `window_start_iso`, `window_end_iso`, `window_end_exclusive: true`
|
||||
```
|
||||
|
||||
5. **[High] Replace fixed default `--since 24m` with contribution-floor auto cutoff**
|
||||
Analysis and rationale: A static window is simple but often over-scans data. Compute a model-derived horizon from a minimum contribution floor (for example `0.01` points) per signal; this keeps results equivalent while reducing query cost.
|
||||
```diff
|
||||
@@ ### 5. Default --since Change
|
||||
-Expert mode: `"6m"` -> `"24m"`
|
||||
+Expert mode default: `--since auto`
|
||||
+`auto` computes earliest relevant timestamp from configured weights/half-lives and `min_contribution_floor`
|
||||
+Add config: `min_contribution_floor` (default: 0.01)
|
||||
+`--since` still overrides, `--all-history` still bypasses cutoff
|
||||
```
|
||||
|
||||
6. **[High] Add bot/service-account filtering now (not later)**
|
||||
Analysis and rationale: Bot activity can materially distort expertise rankings in real repos. This is low implementation cost with high quality gain and should be in v1 of the scoring revamp, not deferred.
|
||||
```diff
|
||||
@@ ### 1. ScoringConfig (config.rs)
|
||||
+ pub excluded_username_patterns: Vec<String>, // default: ["bot","\\[bot\\]","service-account","ci-"]
|
||||
@@ ### 2. SQL Restructure (who.rs)
|
||||
+Apply username exclusion in all signal sources unless `--include-bots` is set
|
||||
@@ ### 5b. Score Explainability via `--explain-score`
|
||||
+Add `filtered_events` counts in robot output metadata
|
||||
```
|
||||
|
||||
7. **[Medium] Enforce deterministic floating-point accumulation**
|
||||
Analysis and rationale: Even with small sets, unordered `HashMap` iteration can cause tiny platform-dependent ranking differences near ties. Sorting contributions and using Neumaier summation removes nondeterminism and stabilizes tests/CI.
|
||||
```diff
|
||||
@@ ### 4. Rust-Side Aggregation (who.rs)
|
||||
-Compute score as `f64`.
|
||||
+Compute score as `f64` using deterministic contribution ordering:
|
||||
+1) sort by (username, signal, mr_id, ts)
|
||||
+2) sum with Neumaier compensation
|
||||
+Tie-break remains `(raw_score DESC, last_seen DESC, username ASC)`
|
||||
```
|
||||
|
||||
8. **[Medium] Strengthen explainability with evidence, not just totals**
|
||||
Analysis and rationale: Component totals help, but disputes usually need “why this user got this score now.” Add compact top evidence rows per component (`mr_id`, `ts`, `raw_contribution`) behind an optional mode.
|
||||
```diff
|
||||
@@ ### 5b. Score Explainability via `--explain-score`
|
||||
-Component breakdown only (4 floats per user).
|
||||
+Add `--explain-score=summary|full`:
|
||||
+`summary`: current 4-component totals
|
||||
+`full`: adds top N evidence rows per component (default N=3)
|
||||
+Robot output includes per-evidence `mr_id`, `signal`, `ts`, `contribution`
|
||||
```
|
||||
|
||||
9. **[Medium] Make query plan strategy explicit: `UNION ALL` default for dual-path scans**
|
||||
Analysis and rationale: You currently treat `UNION ALL` as fallback if planner regresses. For SQLite, OR-across-indexed-columns regressions are common enough that defaulting to branch-split queries is often more predictable.
|
||||
```diff
|
||||
@@ **Index optimization fallback (UNION ALL split)**
|
||||
-Start with the simpler `OR` approach and only switch to `UNION ALL` if query plans confirm degradation.
|
||||
+Use `UNION ALL` + dedup as default for dual-path matching.
|
||||
+Keep `OR` variant as optional strategy flag for benchmarking/regression checks.
|
||||
```
|
||||
|
||||
10. **[Medium] Add explicit performance SLO + benchmark gate**
|
||||
Analysis and rationale: This plan is query-heavy and ranking-critical; add measurable performance budgets so future edits do not silently degrade UX. Include synthetic fixture benchmarks for exact, prefix, and suffix path modes.
|
||||
```diff
|
||||
@@ ## Verification
|
||||
+8. Performance regression gate:
|
||||
+ - `cargo bench --bench who_expert_scoring`
|
||||
+ - Dataset tiers: 100k, 1M, 5M notes
|
||||
+ - SLOs: p95 exact path < 150ms, prefix < 250ms, suffix < 400ms on reference hardware
|
||||
+ - Fail CI if regression > 20% vs stored baseline
|
||||
```
|
||||
|
||||
If you want, I can produce a single consolidated “iteration 5” plan document with these changes already merged into your current structure.
|
||||
134
plans/time-decay-expert-scoring.feedback-6.md
Normal file
134
plans/time-decay-expert-scoring.feedback-6.md
Normal file
@@ -0,0 +1,134 @@
|
||||
I avoided everything already listed in your `Rejected Ideas` section and focused on net-new upgrades.
|
||||
|
||||
1. Centralize MR temporal semantics in one `mr_activity` CTE (architecture + correctness)
|
||||
Why this improves the plan: right now the state-aware timestamp logic is repeated across multiple signal branches, while `closed_mr_multiplier` is applied later in Rust by string state checks. That split is brittle. A single `mr_activity` CTE removes drift risk, simplifies query maintenance, and avoids per-row state-string handling in Rust.
|
||||
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ SQL Restructure
|
||||
+mr_activity AS (
|
||||
+ SELECT
|
||||
+ m.id AS mr_id,
|
||||
+ m.project_id,
|
||||
+ m.author_username,
|
||||
+ m.state,
|
||||
+ CASE
|
||||
+ WHEN m.state = 'merged' THEN COALESCE(m.merged_at, m.created_at)
|
||||
+ WHEN m.state = 'closed' THEN COALESCE(m.closed_at, m.created_at)
|
||||
+ ELSE COALESCE(m.updated_at, m.created_at)
|
||||
+ END AS activity_ts,
|
||||
+ CASE
|
||||
+ WHEN m.state = 'closed' THEN ?5
|
||||
+ ELSE 1.0
|
||||
+ END AS state_mult
|
||||
+ FROM merge_requests m
|
||||
+ WHERE m.state IN ('opened','merged','closed')
|
||||
+),
|
||||
@@
|
||||
-... {state_aware_ts} AS seen_at, m.state AS mr_state
|
||||
+... a.activity_ts AS seen_at, a.state_mult
|
||||
@@
|
||||
-SELECT username, signal, mr_id, qty, ts, mr_state FROM aggregated
|
||||
+SELECT username, signal, mr_id, qty, ts, state_mult FROM aggregated
|
||||
```
|
||||
|
||||
2. Parameterize `reviewer_min_note_chars` and tighten config validation (robustness)
|
||||
Why this improves the plan: inlining `reviewer_min_note_chars` into SQL text creates statement-cache churn and avoidable SQL-text variability. Also, current validation misses finite-range guards (`NaN`, absurd half-lives). Parameterization + stronger validation reduces weird failure modes.
|
||||
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ 1. ScoringConfig (config.rs)
|
||||
- reviewer_min_note_chars must be >= 0
|
||||
+ reviewer_min_note_chars must be <= 4096
|
||||
+ all half-life values must be <= 3650 (10 years safety cap)
|
||||
+ closed_mr_multiplier must be finite and in (0.0, 1.0]
|
||||
@@ SQL Restructure
|
||||
-AND LENGTH(TRIM(COALESCE(n_body.body, ''))) >= {reviewer_min_note_chars}
|
||||
+AND LENGTH(TRIM(COALESCE(n_body.body, ''))) >= ?6
|
||||
```
|
||||
|
||||
3. Add path canonicalization before probes/scoring (correctness + UX)
|
||||
Why this improves the plan: rename-awareness helps only after path resolution succeeds. Inputs like `./src//foo.rs` or inconsistent trailing slashes can still miss. Canonicalizing query paths up front reduces false negatives and ambiguous suffix behavior.
|
||||
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ 3a. Path Resolution Probes (who.rs)
|
||||
+Add `normalize_query_path()` before `build_path_query()`:
|
||||
+- strip leading `./`
|
||||
+- collapse repeated `/`
|
||||
+- trim whitespace
|
||||
+- preserve trailing `/` only for explicit prefix intent
|
||||
+Expose both `path_input_original` and `path_input_normalized` in `resolved_input`.
|
||||
@@ New Tests
|
||||
+test_path_normalization_handles_dot_and_double_slash
|
||||
+test_path_normalization_preserves_explicit_prefix_semantics
|
||||
```
|
||||
|
||||
4. Add epsilon-based tie buckets for stable ranking (determinism)
|
||||
Why this improves the plan: even with deterministic summation order, tiny `powf` platform differences can reorder near-equal scores. Tie bucketing keeps ordering stable and user-meaningful.
|
||||
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ 4. Rust-Side Aggregation (who.rs)
|
||||
-Sort on raw `f64` score — `(raw_score DESC, last_seen DESC, username ASC)`.
|
||||
+Sort using a tie bucket:
|
||||
+`score_bucket = (raw_score / 1e-9).floor() as i64`
|
||||
+Order by `(score_bucket DESC, raw_score DESC, last_seen DESC, username ASC)`.
|
||||
+This preserves precision while preventing meaningless micro-delta reorderings.
|
||||
@@ New Tests
|
||||
+test_near_equal_scores_use_stable_tie_bucket_order
|
||||
```
|
||||
|
||||
5. Add `--diagnose-score` aggregated diagnostics (operability)
|
||||
Why this improves the plan: `--explain-score` tells “why this user scored”, but not “why this query behaved oddly” (path ambiguity, dedup collapse, old_path contribution share, filtered bots, window exclusions). Lightweight aggregate diagnostics are high-value without per-MR drill-down complexity.
|
||||
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ CLI changes (who.rs)
|
||||
+Add `--diagnose-score` flag (compatible with `--explain-score`, incompatible with `--detail`).
|
||||
+When enabled, include:
|
||||
+- matched_notes_raw_count
|
||||
+- matched_notes_dedup_count
|
||||
+- matched_file_changes_raw_count
|
||||
+- matched_file_changes_dedup_count
|
||||
+- rows_excluded_by_window_upper_bound
|
||||
+- users_filtered_by_excluded_usernames
|
||||
+- query_elapsed_ms
|
||||
@@ Robot output
|
||||
+`diagnostics` object emitted only when `--diagnose-score` is set.
|
||||
```
|
||||
|
||||
6. Add probe-optimized indexes for path resolution (performance)
|
||||
Why this improves the plan: current proposed indexes are optimized for scoring joins, but `build_path_query()` and `suffix_probe()` run existence/path-only probes where `author_username` is not constrained. Dedicated probe indexes will materially reduce latency for path lookup modes.
|
||||
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ 6. Index Migration (db.rs)
|
||||
+-- Fast exact/prefix/suffix path probes on notes (no author predicate)
|
||||
+CREATE INDEX IF NOT EXISTS idx_notes_new_path_project_created
|
||||
+ ON notes(position_new_path, project_id, created_at)
|
||||
+ WHERE note_type = 'DiffNote' AND is_system = 0 AND position_new_path IS NOT NULL;
|
||||
+
|
||||
+CREATE INDEX IF NOT EXISTS idx_notes_old_path_project_created
|
||||
+ ON notes(position_old_path, project_id, created_at)
|
||||
+ WHERE note_type = 'DiffNote' AND is_system = 0 AND position_old_path IS NOT NULL;
|
||||
```
|
||||
|
||||
7. Add multi-path expert scoring (`--path` repeatable) with dedup across paths (feature + utility)
|
||||
Why this improves the plan: current model is single-path centric. Real ownership questions are usually subsystem-level. Repeatable paths/prefixes let users ask “who knows auth stack?” in one call. Dedup by `(username, signal, mr_id)` avoids double-counting same MR touching multiple requested paths.
|
||||
|
||||
```diff
|
||||
diff --git a/plan.md b/plan.md
|
||||
@@ CLI/feature scope
|
||||
+Add repeatable `--path` in expert mode:
|
||||
+`lore who --expert --path src/auth/ --path src/session/`
|
||||
+Optional `--path-file <file>` for large path sets (one per line).
|
||||
@@ SQL Restructure
|
||||
+Add `requested_paths` CTE and match each source against that set.
|
||||
+Ensure dedup key includes `(username, signal, mr_id)` so one MR contributes once per signal even if multiple paths match.
|
||||
@@ New Tests
|
||||
+test_multi_path_query_unions_results_without_double_counting
|
||||
+test_multi_path_with_overlap_prefixes_is_idempotent
|
||||
```
|
||||
|
||||
These 7 revisions keep your current model direction intact, but reduce correctness drift risk, harden edge handling, improve query observability, and make the feature materially more useful for real ownership workflows.
|
||||
688
plans/time-decay-expert-scoring.md
Normal file
688
plans/time-decay-expert-scoring.md
Normal file
@@ -0,0 +1,688 @@
|
||||
---
|
||||
plan: true
|
||||
title: ""
|
||||
status: iterating
|
||||
iteration: 6
|
||||
target_iterations: 8
|
||||
beads_revision: 1
|
||||
related_plans: []
|
||||
created: 2026-02-08
|
||||
updated: 2026-02-12
|
||||
---
|
||||
|
||||
# Time-Decay Expert Scoring Model
|
||||
|
||||
## Context
|
||||
|
||||
The `lore who --path` command currently uses flat weights to score expertise: each authored MR counts as 25 points, each reviewed MR as 10, each inline note as 1 — regardless of when the activity happened. This produces three compounding problems:
|
||||
|
||||
1. **Temporal blindness**: Old activity counts the same as recent activity. Someone who authored a file 2 years ago ranks equivalently to someone who wrote it last week.
|
||||
2. **Reviewer inflation**: Senior reviewers (jdefting, zhayes) who rubber-stamp every MR via assignment accumulate inflated scores indistinguishable from reviewers who actually left substantive inline feedback. The `mr_reviewers` table captures assignment, not engagement.
|
||||
3. **Path-history blindness**: Renamed or moved files lose historical expertise because signal matching relies on `position_new_path` and `mr_file_changes.new_path` only. A developer who authored the file under its previous name gets zero credit after a rename.
|
||||
|
||||
The fix has three parts:
|
||||
- Apply **exponential half-life decay** to each signal, grounded in cognitive science research
|
||||
- **Split the reviewer signal** into "participated" (left DiffNotes) vs "assigned-only" (in `mr_reviewers` but no inline comments), with different weights and decay rates
|
||||
- **Match both old and new paths** in all signal queries AND path resolution probes so expertise survives file renames
|
||||
|
||||
## Research Foundation
|
||||
|
||||
- **Ebbinghaus Forgetting Curve (1885)**: Memory retention follows exponential decay: `R = 2^(-t/h)` where h is the half-life
|
||||
- **Generation Effect (Slamecka & Graf, 1978)**: Producing information (authoring code) creates ~2x more durable memory traces than reading it (reviewing)
|
||||
- **Levels of Processing (Craik & Lockhart, 1972)**: Deeper cognitive engagement creates more durable memories — authoring > reviewing > commenting
|
||||
- **Half-Life Regression (Settles & Meeder, 2016, Duolingo)**: Exponential decay with per-signal-type half-lives is practical and effective at scale. Chosen over power law for additivity, bounded behavior, and intuitive parameterization
|
||||
- **Fritz et al. (2010, ICSE)**: "Degree-of-knowledge" model for code familiarity considers both authoring and interaction events with time-based decay
|
||||
|
||||
## Scoring Formula
|
||||
|
||||
```
|
||||
score(user, path) = Sum_i( weight_i * 2^(-days_elapsed_i / half_life_i) )
|
||||
```
|
||||
|
||||
For note signals grouped per MR, a diminishing-returns function caps comment storms:
|
||||
```
|
||||
note_contribution(mr) = note_bonus * log2(1 + note_count_in_mr) * 2^(-days_elapsed / note_half_life)
|
||||
```
|
||||
|
||||
**Why `log2` instead of `ln`?** With `log2`, a single note contributes exactly `note_bonus * 1.0` (since `log2(2) = 1`), making the `note_bonus` weight directly interpretable as "points per note at count=1." With `ln`, one note contributes `note_bonus * 0.69`, which is unintuitive and means `note_bonus=1` doesn't actually mean "1 point per note." The diminishing-returns curve shape is identical — only the scale factor differs.
|
||||
|
||||
Per-signal contributions (each signal is either per-MR or per-note-group):
|
||||
|
||||
| Signal Type | Base Weight | Half-Life | Rationale |
|
||||
|-------------|-------------|-----------|-----------|
|
||||
| **Author** (authored MR touching path) | 25 | 180 days | Deep generative engagement; ~50% retention at 6 months |
|
||||
| **Reviewer Participated** (left DiffNote on MR/path) | 10 | 90 days | Active review engagement; ~50% at 3 months |
|
||||
| **Reviewer Assigned-Only** (in `mr_reviewers`, no DiffNote on path) | 3 | 45 days | Passive assignment; minimal cognitive engagement, fades fast |
|
||||
| **Note** (inline DiffNotes on path, grouped per MR) | 1 | 45 days | `log2(1+count)` per MR; diminishing returns prevent comment storms |
|
||||
|
||||
**Why split reviewers?** The `mr_reviewers` table records assignment, not engagement. A reviewer who left 5 inline comments on a file has demonstrably more expertise than one who was merely assigned and clicked "approve." The participated signal inherits the old reviewer weight (10) and decay (90 days); the assigned-only signal gets reduced weight (3) and faster decay (45 days) — enough to register but not enough to inflate past actual contributors.
|
||||
|
||||
**Why require substantive notes?** Participation is qualified by a minimum note body length (`reviewer_min_note_chars`, default 20). Without this, a single "LGTM" or "+1" comment would promote a reviewer from the 3-point assigned-only tier to the 10-point participated tier — a 3.3x weight increase for zero substantive engagement. The threshold is configurable to accommodate teams with different review conventions.
|
||||
|
||||
**Why cap notes per MR?** Without diminishing returns, a back-and-forth thread of 30 comments on a single MR would score 30 note points — disproportionate to the expertise gained. `log2(1 + 30) ≈ 4.95` vs `log2(1 + 1) = 1.0` preserves the signal that more comments = more engagement while preventing outlier MRs from dominating. The 30-note reviewer gets ~5x the credit of a 1-note reviewer, not 30x.
|
||||
|
||||
Author/reviewer signals are deduplicated per MR (one signal per distinct MR). Note signals are grouped per (user, MR) and use `log2(1 + count)` scaling.
|
||||
|
||||
**Why include closed MRs?** Closed-without-merge MRs represent real review effort and code familiarity even though the code was abandoned. All signals from closed MRs are multiplied by `closed_mr_multiplier` (default 0.5) to reflect this reduced but non-zero contribution. This applies uniformly to author, reviewer, and note signals on closed MRs.
|
||||
|
||||
## Files to Modify
|
||||
|
||||
1. **`src/core/config.rs`** — Add half-life fields + assigned-only reviewer config to `ScoringConfig`; add config validation
|
||||
2. **`src/cli/commands/who.rs`** — Core changes:
|
||||
- Add `half_life_decay()` pure function
|
||||
- Add `normalize_query_path()` for input canonicalization before path resolution
|
||||
- Restructure `query_expert()`: SQL returns hybrid-aggregated signal rows with timestamps and state multiplier (MR-level for author/reviewer, note-count-per-MR for notes), Rust applies decay + `log2(1+count)` + final ranking
|
||||
- Match both `new_path` and `old_path` in all signal queries (rename awareness)
|
||||
- Extend rename awareness to `build_path_query()` probes and `suffix_probe()` (not just scoring)
|
||||
- Split reviewer signal into participated vs assigned-only
|
||||
- Use state-aware timestamps (`merged_at` for merged MRs, `updated_at` for open MRs)
|
||||
- Change default `--since` from `"6m"` to `"24m"` (2 years captures all meaningful decayed signals)
|
||||
- Add `--as-of` flag for reproducible scoring at a fixed timestamp
|
||||
- Add `--explain-score` flag for per-user score component breakdown
|
||||
- Add `--include-bots` flag to disable bot/service-account filtering
|
||||
- Sort on raw f64 score, round only for display
|
||||
- Update tests
|
||||
3. **`src/core/db.rs`** — Add migration for indexes supporting the new query shapes (dual-path matching, reviewer participation CTE, path resolution probes)
|
||||
|
||||
## Implementation Details
|
||||
|
||||
### 1. ScoringConfig (config.rs)
|
||||
|
||||
Add half-life fields and the new assigned-only reviewer signal. All new fields use `#[serde(default)]` for backward compatibility:
|
||||
|
||||
```rust
|
||||
pub struct ScoringConfig {
|
||||
pub author_weight: i64, // default: 25
|
||||
pub reviewer_weight: i64, // default: 10 (participated — left DiffNotes)
|
||||
pub reviewer_assignment_weight: i64, // default: 3 (assigned-only — no DiffNotes on path)
|
||||
pub note_bonus: i64, // default: 1
|
||||
pub author_half_life_days: u32, // default: 180
|
||||
pub reviewer_half_life_days: u32, // default: 90 (participated)
|
||||
pub reviewer_assignment_half_life_days: u32, // default: 45 (assigned-only)
|
||||
pub note_half_life_days: u32, // default: 45
|
||||
pub closed_mr_multiplier: f64, // default: 0.5 (applied to closed-without-merge MRs)
|
||||
pub reviewer_min_note_chars: u32, // default: 20 (minimum note body length to count as participation)
|
||||
pub excluded_usernames: Vec<String>, // default: [] (exact-match usernames to exclude, e.g. ["renovate-bot", "gitlab-ci"])
|
||||
}
|
||||
```
|
||||
|
||||
**Config validation**: Add a `validate_scoring()` call in `Config::load_from_path()` after deserialization:
|
||||
- All `*_half_life_days` must be > 0 and <= 3650 (prevents division by zero in decay function; rejects absurd 10+ year half-lives that would effectively disable decay)
|
||||
- All `*_weight` / `*_bonus` must be >= 0 (negative weights produce nonsensical scores)
|
||||
- `closed_mr_multiplier` must be finite (not NaN/Inf) and in `(0.0, 1.0]` (0 would discard closed MRs entirely; >1 would over-weight them; NaN/Inf would propagate through all scores)
|
||||
- `reviewer_min_note_chars` must be >= 0 and <= 4096 (0 disables the filter; 4096 is a sane upper bound — no real review comment needs to be longer to qualify; typical useful values: 10-50)
|
||||
- `excluded_usernames` entries must be non-empty strings (no blank entries)
|
||||
- Return `LoreError::ConfigInvalid` with a clear message on failure
|
||||
|
||||
### 2. Decay Function (who.rs)
|
||||
|
||||
```rust
|
||||
fn half_life_decay(elapsed_ms: i64, half_life_days: u32) -> f64 {
|
||||
let days = (elapsed_ms as f64 / 86_400_000.0).max(0.0);
|
||||
let hl = f64::from(half_life_days);
|
||||
if hl <= 0.0 { return 0.0; }
|
||||
2.0_f64.powf(-days / hl)
|
||||
}
|
||||
```
|
||||
|
||||
### 3. SQL Restructure (who.rs)
|
||||
|
||||
The SQL uses **CTE-based dual-path matching**, a **centralized `mr_activity` CTE**, and **hybrid aggregation**. Rather than repeating `OR old_path` in every signal subquery, two foundational CTEs (`matched_notes`, `matched_file_changes`) centralize path matching. A `mr_activity` CTE centralizes the state-aware timestamp and state multiplier in one place, eliminating repetition of the CASE expression across signals 3, 4a, 4b. A fourth CTE (`reviewer_participation`) precomputes which reviewers actually left DiffNotes, avoiding correlated `EXISTS`/`NOT EXISTS` subqueries.
|
||||
|
||||
MR-level signals return one row per (username, signal, mr_id) with a timestamp and state multiplier; note signals return one row per (username, mr_id) with `note_count` and `max_ts`. This keeps row counts bounded (dozens to low hundreds per path) while giving Rust the data it needs for decay and `log2(1+count)`.
|
||||
|
||||
```sql
|
||||
WITH matched_notes_raw AS (
|
||||
-- Branch 1: match on new_path (uses idx_notes_new_path or equivalent)
|
||||
SELECT n.id, n.discussion_id, n.author_username, n.created_at, n.project_id
|
||||
FROM notes n
|
||||
WHERE n.note_type = 'DiffNote'
|
||||
AND n.is_system = 0
|
||||
AND n.author_username IS NOT NULL
|
||||
AND n.created_at >= ?2
|
||||
AND n.created_at < ?4
|
||||
AND (?3 IS NULL OR n.project_id = ?3)
|
||||
AND n.position_new_path {path_op}
|
||||
UNION ALL
|
||||
-- Branch 2: match on old_path (uses idx_notes_old_path_author)
|
||||
SELECT n.id, n.discussion_id, n.author_username, n.created_at, n.project_id
|
||||
FROM notes n
|
||||
WHERE n.note_type = 'DiffNote'
|
||||
AND n.is_system = 0
|
||||
AND n.author_username IS NOT NULL
|
||||
AND n.created_at >= ?2
|
||||
AND n.created_at < ?4
|
||||
AND (?3 IS NULL OR n.project_id = ?3)
|
||||
AND n.position_old_path {path_op}
|
||||
),
|
||||
matched_notes AS (
|
||||
-- Dedup: prevent double-counting when old_path = new_path (no rename)
|
||||
SELECT DISTINCT id, discussion_id, author_username, created_at, project_id
|
||||
FROM matched_notes_raw
|
||||
),
|
||||
matched_file_changes_raw AS (
|
||||
-- Branch 1: match on new_path (uses idx_mfc_new_path_project_mr)
|
||||
SELECT fc.merge_request_id, fc.project_id
|
||||
FROM mr_file_changes fc
|
||||
WHERE (?3 IS NULL OR fc.project_id = ?3)
|
||||
AND fc.new_path {path_op}
|
||||
UNION ALL
|
||||
-- Branch 2: match on old_path (uses idx_mfc_old_path_project_mr)
|
||||
SELECT fc.merge_request_id, fc.project_id
|
||||
FROM mr_file_changes fc
|
||||
WHERE (?3 IS NULL OR fc.project_id = ?3)
|
||||
AND fc.old_path {path_op}
|
||||
),
|
||||
matched_file_changes AS (
|
||||
-- Dedup: prevent double-counting when old_path = new_path (no rename)
|
||||
SELECT DISTINCT merge_request_id, project_id
|
||||
FROM matched_file_changes_raw
|
||||
),
|
||||
mr_activity AS (
|
||||
-- Centralized state-aware timestamps and state multiplier.
|
||||
-- Defined once, referenced by all file-change-based signals (3, 4a, 4b).
|
||||
-- Scoped to MRs matched by file changes to avoid materializing the full MR table.
|
||||
SELECT DISTINCT
|
||||
m.id AS mr_id,
|
||||
m.author_username,
|
||||
m.state,
|
||||
CASE
|
||||
WHEN m.state = 'merged' THEN COALESCE(m.merged_at, m.created_at)
|
||||
WHEN m.state = 'closed' THEN COALESCE(m.closed_at, m.created_at)
|
||||
ELSE COALESCE(m.updated_at, m.created_at)
|
||||
END AS activity_ts,
|
||||
CASE WHEN m.state = 'closed' THEN ?5 ELSE 1.0 END AS state_mult
|
||||
FROM merge_requests m
|
||||
JOIN matched_file_changes mfc ON mfc.merge_request_id = m.id
|
||||
WHERE m.state IN ('opened','merged','closed')
|
||||
),
|
||||
reviewer_participation AS (
|
||||
-- Precompute which (mr_id, username) pairs have substantive DiffNote participation.
|
||||
-- Materialized once, then joined against mr_reviewers to classify.
|
||||
-- The LENGTH filter excludes trivial notes ("LGTM", "+1", emoji-only) from qualifying
|
||||
-- a reviewer as "participated." Without this, a single "LGTM" would promote an assigned
|
||||
-- reviewer from 3-point to 10-point weight, defeating the purpose of the split.
|
||||
-- Note: mn.id refers back to notes.id, so we join notes to access the body column
|
||||
-- (not carried in matched_notes to avoid bloating that CTE with body text).
|
||||
-- ?6 is the configured reviewer_min_note_chars value (default 20).
|
||||
SELECT DISTINCT d.merge_request_id AS mr_id, mn.author_username AS username
|
||||
FROM matched_notes mn
|
||||
JOIN discussions d ON mn.discussion_id = d.id
|
||||
JOIN notes n_body ON mn.id = n_body.id
|
||||
WHERE d.merge_request_id IS NOT NULL
|
||||
AND LENGTH(TRIM(COALESCE(n_body.body, ''))) >= ?6
|
||||
),
|
||||
raw AS (
|
||||
-- Signal 1: DiffNote reviewer (individual notes for note_cnt)
|
||||
-- Computes state_mult inline (not via mr_activity) because this joins through discussions, not file changes.
|
||||
SELECT mn.author_username AS username, 'diffnote_reviewer' AS signal,
|
||||
m.id AS mr_id, mn.id AS note_id, mn.created_at AS seen_at,
|
||||
CASE WHEN m.state = 'closed' THEN ?5 ELSE 1.0 END AS state_mult
|
||||
FROM matched_notes mn
|
||||
JOIN discussions d ON mn.discussion_id = d.id
|
||||
JOIN merge_requests m ON d.merge_request_id = m.id
|
||||
WHERE (m.author_username IS NULL OR mn.author_username != m.author_username)
|
||||
AND m.state IN ('opened','merged','closed')
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Signal 2: DiffNote MR author
|
||||
-- Computes state_mult inline (same reason as signal 1).
|
||||
SELECT m.author_username AS username, 'diffnote_author' AS signal,
|
||||
m.id AS mr_id, NULL AS note_id, MAX(mn.created_at) AS seen_at,
|
||||
CASE WHEN m.state = 'closed' THEN ?5 ELSE 1.0 END AS state_mult
|
||||
FROM merge_requests m
|
||||
JOIN discussions d ON d.merge_request_id = m.id
|
||||
JOIN matched_notes mn ON mn.discussion_id = d.id
|
||||
WHERE m.author_username IS NOT NULL
|
||||
AND m.state IN ('opened','merged','closed')
|
||||
GROUP BY m.author_username, m.id
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Signal 3: MR author via file changes (uses mr_activity CTE for timestamp + state_mult)
|
||||
SELECT a.author_username AS username, 'file_author' AS signal,
|
||||
a.mr_id, NULL AS note_id,
|
||||
a.activity_ts AS seen_at, a.state_mult
|
||||
FROM mr_activity a
|
||||
WHERE a.author_username IS NOT NULL
|
||||
AND a.activity_ts >= ?2
|
||||
AND a.activity_ts < ?4
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Signal 4a: Reviewer participated (in mr_reviewers AND left DiffNotes on path)
|
||||
SELECT r.username AS username, 'file_reviewer_participated' AS signal,
|
||||
a.mr_id, NULL AS note_id,
|
||||
a.activity_ts AS seen_at, a.state_mult
|
||||
FROM mr_activity a
|
||||
JOIN mr_reviewers r ON r.merge_request_id = a.mr_id
|
||||
JOIN reviewer_participation rp ON rp.mr_id = a.mr_id AND rp.username = r.username
|
||||
WHERE r.username IS NOT NULL
|
||||
AND (a.author_username IS NULL OR r.username != a.author_username)
|
||||
AND a.activity_ts >= ?2
|
||||
AND a.activity_ts < ?4
|
||||
|
||||
UNION ALL
|
||||
|
||||
-- Signal 4b: Reviewer assigned-only (in mr_reviewers, NO DiffNotes on path)
|
||||
SELECT r.username AS username, 'file_reviewer_assigned' AS signal,
|
||||
a.mr_id, NULL AS note_id,
|
||||
a.activity_ts AS seen_at, a.state_mult
|
||||
FROM mr_activity a
|
||||
JOIN mr_reviewers r ON r.merge_request_id = a.mr_id
|
||||
LEFT JOIN reviewer_participation rp ON rp.mr_id = a.mr_id AND rp.username = r.username
|
||||
WHERE rp.username IS NULL -- NOT in participation set
|
||||
AND r.username IS NOT NULL
|
||||
AND (a.author_username IS NULL OR r.username != a.author_username)
|
||||
AND a.activity_ts >= ?2
|
||||
AND a.activity_ts < ?4
|
||||
),
|
||||
aggregated AS (
|
||||
-- MR-level signals: 1 row per (username, signal_class, mr_id) with MAX(ts)
|
||||
SELECT username, signal, mr_id, 1 AS qty, MAX(seen_at) AS ts, MAX(state_mult) AS state_mult
|
||||
FROM raw WHERE signal != 'diffnote_reviewer'
|
||||
GROUP BY username, signal, mr_id
|
||||
UNION ALL
|
||||
-- Note signals: 1 row per (username, mr_id) with note_count and max_ts
|
||||
SELECT username, 'note_group' AS signal, mr_id, COUNT(*) AS qty, MAX(seen_at) AS ts, MAX(state_mult) AS state_mult
|
||||
FROM raw WHERE signal = 'diffnote_reviewer' AND note_id IS NOT NULL
|
||||
GROUP BY username, mr_id
|
||||
)
|
||||
SELECT username, signal, mr_id, qty, ts, state_mult FROM aggregated WHERE username IS NOT NULL
|
||||
```
|
||||
|
||||
Where `{path_op}` is either `= ?1` or `LIKE ?1 ESCAPE '\\'` depending on the path query type, `?2` is `since_ms`, `?3` is the optional project_id, `?4` is the `as_of_ms` exclusive upper bound (defaults to `now_ms` when `--as-of` is not specified), `?5` is the `closed_mr_multiplier` (default 0.5, bound as a parameter), and `?6` is the configured `reviewer_min_note_chars` value (default 20, bound as a parameter). The `>= ?2 AND < ?4` pattern (half-open interval) ensures that when `--as-of` is set to a past date, events at or after that date are excluded — without this, "future" events would leak in with full weight, breaking reproducibility. The exclusive upper bound avoids edge-case ambiguity when events have timestamps exactly equal to the as-of value.
|
||||
|
||||
**Rationale for CTE-based dual-path matching**: The previous approach (repeating `OR old_path` in every signal subquery) duplicated the path matching logic 5 times. Factoring it into foundational CTEs (`matched_notes_raw` → `matched_notes`, `matched_file_changes_raw` → `matched_file_changes`) means path matching is defined once, each index branch is explicit, and adding future path resolution logic (e.g., alias chains) only requires changes in one place. The UNION ALL + dedup pattern ensures SQLite uses the optimal index for each path column independently.
|
||||
|
||||
**Dual-path matching strategy (UNION ALL split)**: SQLite's query planner commonly struggles with `OR` across two indexed columns, falling back to a full table scan instead of using either index. Rather than starting with `OR` and hoping the planner cooperates, use `UNION ALL` + dedup as the default strategy:
|
||||
```sql
|
||||
matched_notes AS (
|
||||
SELECT ... FROM notes n WHERE ... AND n.position_new_path {path_op}
|
||||
UNION ALL
|
||||
SELECT ... FROM notes n WHERE ... AND n.position_old_path {path_op}
|
||||
),
|
||||
matched_notes_dedup AS (
|
||||
SELECT DISTINCT id, discussion_id, author_username, created_at, project_id
|
||||
FROM matched_notes
|
||||
),
|
||||
```
|
||||
This ensures each branch can use its respective index independently. The dedup CTE prevents double-counting when `old_path = new_path` (no rename). The same pattern applies to `matched_file_changes`. The simpler `OR` variant is retained as a comment for benchmarking — if a future SQLite version handles `OR` well, the split can be collapsed.
|
||||
|
||||
**Rationale for precomputed participation set**: The previous approach used correlated `EXISTS`/`NOT EXISTS` subqueries to classify reviewers. The `reviewer_participation` CTE materializes the set of `(mr_id, username)` pairs from matched DiffNotes once, then signal 4a JOINs against it (participated) and signal 4b LEFT JOINs with `IS NULL` (assigned-only). This avoids per-reviewer-row correlated scans, is easier to reason about, and produces the same exhaustive split — every `mr_reviewers` row falls into exactly one bucket.
|
||||
|
||||
**Rationale for hybrid over fully-raw**: Pre-aggregating note counts in SQL prevents row explosion from heavy DiffNote volume on frequently-discussed paths. MR-level signals are already 1-per-MR by nature (deduped via GROUP BY in each subquery). This keeps memory and latency predictable regardless of review activity density.
|
||||
|
||||
**Path rename awareness**: Both `matched_notes` and `matched_file_changes` use UNION ALL + dedup to match against both old and new path columns independently, ensuring each branch uses its respective index:
|
||||
|
||||
- Notes: branch 1 matches `position_new_path`, branch 2 matches `position_old_path`, deduped by `notes.id`
|
||||
- File changes: branch 1 matches `new_path`, branch 2 matches `old_path`, deduped by `(merge_request_id, project_id)`
|
||||
|
||||
Both columns already exist in the schema (`notes.position_old_path` from migration 002, `mr_file_changes.old_path` from migration 016). The UNION ALL approach ensures expertise is credited even when a file was renamed after the work was done. For prefix queries (`--path src/foo/`), the `LIKE` operator applies to both columns identically.
|
||||
|
||||
**Signal 4 splits into two**: The current signal 4 (`file_reviewer`) joins `mr_reviewers` but doesn't distinguish participation. In the new plan:
|
||||
|
||||
- **Signal 4a** (`file_reviewer_participated`): User is in `mr_reviewers` AND appears in the `reviewer_participation` CTE (left DiffNotes on the path for that MR). Gets `reviewer_weight` (10) and `reviewer_half_life_days` (90).
|
||||
- **Signal 4b** (`file_reviewer_assigned`): User is in `mr_reviewers` but NOT in the `reviewer_participation` CTE. Gets `reviewer_assignment_weight` (3) and `reviewer_assignment_half_life_days` (45).
|
||||
|
||||
**Rationale for `mr_activity` CTE**: The previous approach repeated the state-aware CASE expression and `m.state` column in signals 3, 4a, and 4b, with the `closed_mr_multiplier` applied later in Rust by string-matching on `mr_state`. This split was brittle — the CASE expression could drift between signal branches, and per-row state-string handling in Rust was unnecessary indirection. The `mr_activity` CTE defines the timestamp and multiplier once, scoped to matched MRs only (via JOIN with `matched_file_changes`) to avoid materializing the full MR table. Signals 3, 4a, 4b now reference `a.activity_ts` and `a.state_mult` directly. Signals 1 and 2 (DiffNote-based) still compute `state_mult` inline because they join through `discussions`, not `matched_file_changes`, and adding them to `mr_activity` would require a second join path that doesn't simplify anything.
|
||||
|
||||
**Rationale for parameterized `reviewer_min_note_chars` and `closed_mr_multiplier`**: Previous iterations inlined `reviewer_min_note_chars` as a literal in the SQL string and kept `closed_mr_multiplier` in Rust only. Binding both as SQL parameters (`?5` for `closed_mr_multiplier`, `?6` for `reviewer_min_note_chars`) eliminates statement-cache churn (the SQL text is identical regardless of config values), avoids SQL-text variability that complicates EXPLAIN QUERY PLAN analysis, and centralizes the multiplier application in SQL for file-change signals. The DiffNote signals (1, 2) still compute `state_mult` inline because they don't go through `mr_activity`.
|
||||
|
||||
### 3a. Path Canonicalization and Resolution Probes (who.rs)
|
||||
|
||||
**Path canonicalization**: Before any path resolution or scoring, normalize the user's input path via `normalize_query_path()`:
|
||||
- Strip leading `./` (e.g., `./src/foo.rs` → `src/foo.rs`)
|
||||
- Collapse repeated `/` (e.g., `src//foo.rs` → `src/foo.rs`)
|
||||
- Trim leading/trailing whitespace
|
||||
- Preserve trailing `/` only when present — it signals explicit prefix intent
|
||||
|
||||
This is applied once at the top of `run_who()` before `build_path_query()`. The robot JSON `resolved_input` includes both `path_input_original` (raw user input) and `path_input_normalized` (after canonicalization) for debugging transparency. The normalization is purely syntactic — no filesystem lookups, no canonicalization against the database.
|
||||
|
||||
**Path resolution probes**: Rename awareness must extend beyond scoring queries to the path resolution layer. Currently `build_path_query()` (line 457) and `suffix_probe()` (line 584) only check `position_new_path` and `new_path`. If a user queries an old path name, these probes return "not found" and the scoring query never runs.
|
||||
|
||||
Rename awareness must extend beyond scoring queries to the path resolution layer. Currently `build_path_query()` (line 457) and `suffix_probe()` (line 584) only check `position_new_path` and `new_path`. If a user queries an old path name, these probes return "not found" and the scoring query never runs.
|
||||
|
||||
**Changes to `build_path_query()`**:
|
||||
|
||||
- **Probe 1 (exact_exists)**: Add `OR position_old_path = ?1` to the notes query and `OR old_path = ?1` to the `mr_file_changes` query. This detects files that existed under the queried name even if they've since been renamed.
|
||||
- **Probe 2 (prefix_exists)**: Add `OR position_old_path LIKE ?1 ESCAPE '\\'` and `OR old_path LIKE ?1 ESCAPE '\\'` to the respective queries.
|
||||
|
||||
**Changes to `suffix_probe()`**:
|
||||
|
||||
The UNION query inside `suffix_probe()` currently only selects `position_new_path` from notes and `new_path` from file changes. Add two additional UNION branches:
|
||||
|
||||
```sql
|
||||
UNION
|
||||
SELECT position_old_path AS full_path FROM notes
|
||||
WHERE note_type = 'DiffNote' AND is_system = 0
|
||||
AND position_old_path IS NOT NULL
|
||||
AND (position_old_path LIKE ?1 ESCAPE '\\' OR position_old_path = ?2)
|
||||
AND (?3 IS NULL OR project_id = ?3)
|
||||
UNION
|
||||
SELECT old_path AS full_path FROM mr_file_changes
|
||||
WHERE old_path IS NOT NULL
|
||||
AND (old_path LIKE ?1 ESCAPE '\\' OR old_path = ?2)
|
||||
AND (?3 IS NULL OR project_id = ?3)
|
||||
```
|
||||
|
||||
This ensures that querying by an old filename (e.g., `login.rs` after it was renamed to `auth.rs`) still resolves to a usable path for scoring. The UNION deduplicates so the same path appearing in both old and new columns doesn't cause false ambiguity.
|
||||
|
||||
**State-aware timestamps for file-change signals (signals 3, 4a, 4b)**: Centralized in the `mr_activity` CTE (see section 3). The CASE expression uses `merged_at` for merged MRs, `closed_at` for closed MRs, and `updated_at` for open MRs, with `created_at` as fallback when the preferred timestamp is NULL.
|
||||
|
||||
**Rationale**: `updated_at` is noisy for merged MRs — it changes on label edits, title changes, rebases, and metadata touches, creating false recency. `merged_at` is the best indicator of when code expertise was formed (the moment the code entered the branch). But for **open MRs**, `updated_at` is actually the right signal because it reflects ongoing active work. `closed_at` anchors closed-without-merge MRs to their closure time (these represent review effort even if the code was abandoned). Each state gets the timestamp that best represents when expertise was last exercised.
|
||||
|
||||
### 4. Rust-Side Aggregation (who.rs)
|
||||
|
||||
For each username, accumulate into a struct with:
|
||||
- **Author MRs**: `HashMap<i64, (i64, f64)>` (mr_id -> (max timestamp, state_mult)) from `diffnote_author` + `file_author` signals
|
||||
- **Reviewer Participated MRs**: `HashMap<i64, (i64, f64)>` from `diffnote_reviewer` + `file_reviewer_participated` signals
|
||||
- **Reviewer Assigned-Only MRs**: `HashMap<i64, (i64, f64)>` from `file_reviewer_assigned` signals (excluding any MR already in participated set)
|
||||
- **Notes per MR**: `HashMap<i64, (u32, i64, f64)>` (mr_id -> (count, max_ts, state_mult)) from `note_group` rows in the aggregated query (already grouped per user+MR with note_count in `qty`). Used for `log2(1 + count)` diminishing returns.
|
||||
- **Last seen**: max of all timestamps
|
||||
- **Components** (when `--explain-score`): Track per-component f64 subtotals for `author`, `reviewer_participated`, `reviewer_assigned`, `notes`
|
||||
|
||||
The `state_mult` field from each SQL row (already computed in SQL as 1.0 for merged/open or `closed_mr_multiplier` for closed) is stored alongside the timestamp — no string-matching on MR state needed in Rust.
|
||||
|
||||
Compute score as `f64` with **deterministic contribution ordering**: within each signal type, sort contributions by `(mr_id ASC)` before summing. This eliminates platform-dependent HashMap iteration order as a source of f64 rounding variance near ties, ensuring CI reproducibility without the complexity of compensated summation (Neumaier/Kahan). Each MR-level contribution is multiplied by its `state_mult` (already computed in SQL):
|
||||
```
|
||||
raw_score =
|
||||
sum(author_weight * state_mult * decay(now - ts, author_hl) for (mr, ts, state_mult) in author_mrs)
|
||||
+ sum(reviewer_weight * state_mult * decay(now - ts, reviewer_hl) for (mr, ts, state_mult) in reviewer_participated)
|
||||
+ sum(reviewer_assignment_weight * state_mult * decay(now - ts, reviewer_assignment_hl) for (mr, ts, state_mult) in reviewer_assigned)
|
||||
+ sum(note_bonus * state_mult * log2(1 + count) * decay(now - ts, note_hl) for (mr, count, ts, state_mult) in notes_per_mr)
|
||||
```
|
||||
|
||||
**Why include closed MRs?** A closed-without-merge MR still represents review effort and code familiarity — the reviewer read the diff, left comments, and engaged with the code even though it was ultimately abandoned. Excluding closed MRs entirely (the previous plan's approach) discarded this signal. The `closed_mr_multiplier` (default 0.5) halves the contribution, reflecting that the code never landed but the reviewer's cognitive engagement was real. This also eliminates the dead-code inconsistency where the state-aware CASE expression handled `closed` but the WHERE clause excluded it.
|
||||
|
||||
**Sort on raw `f64` score** — `(raw_score DESC, last_seen DESC, username ASC)`. This prevents false ties from premature rounding. Only round to `i64` for the `Expert.score` display field after sorting and truncation. The robot JSON `score` field stays integer for backward compatibility. When `--explain-score` is active, also include `score_raw` (the unrounded f64) alongside `score` so the component totals can be verified without rounding noise.
|
||||
|
||||
Compute counts from the accumulated data:
|
||||
- `review_mr_count = reviewer_participated.len() + reviewer_assigned.len()`
|
||||
- `review_note_count = notes_per_mr.values().map(|(count, _)| count).sum()`
|
||||
- `author_mr_count = author_mrs.len()`
|
||||
|
||||
**Bot/service-account filtering**: After accumulating all user scores and before sorting, filter out any username that appears in `config.scoring.excluded_usernames` (exact match, case-insensitive). This is applied in Rust post-query (not SQL) to keep the SQL clean and avoid parameter explosion. When `--include-bots` is active, the filter is skipped entirely. The robot JSON `resolved_input` includes `excluded_usernames_applied: true|false` to indicate whether filtering was active.
|
||||
|
||||
Truncate to limit after sorting.
|
||||
|
||||
### 5. Default --since Change
|
||||
|
||||
Expert mode: `"6m"` -> `"24m"` (line 289 in who.rs).
|
||||
At 2 years, author decay = 6%, reviewer decay = 0.4%, note decay = 0.006% — negligible, good cutoff.
|
||||
|
||||
**Diagnostic escape hatch**: Add `--all-history` flag (conflicts with `--since`) that sets `since_ms = 0`, capturing all data regardless of age. Useful for debugging scoring anomalies and validating the decay model against known experts. The `since_mode` field in robot JSON reports `"all"` when this flag is active.
|
||||
|
||||
### 5a. Reproducible Scoring via `--as-of`
|
||||
|
||||
Add `--as-of <RFC3339|YYYY-MM-DD>` flag that overrides the `now_ms` reference point used for decay calculations. When set:
|
||||
- All event selection is bounded by `[since_ms, as_of_ms)` — exclusive upper bound; events at or after `as_of_ms` are excluded from SQL results entirely (not just decayed). The SQL uses `< ?4` (strict less-than), not `<= ?4`.
|
||||
- `YYYY-MM-DD` input (without time component) is interpreted as end-of-day UTC: `T23:59:59.999Z`. This matches user intuition that `--as-of 2025-06-01` means "as of the end of June 1st" rather than "as of midnight at the start of June 1st" which would exclude the entire day's activity.
|
||||
- All decay computations use `as_of_ms` instead of `SystemTime::now()`
|
||||
- The `--since` window is calculated relative to `as_of_ms` (not wall clock)
|
||||
- Robot JSON `resolved_input` includes `as_of_ms`, `as_of_iso`, `window_start_iso`, `window_end_iso`, and `window_end_exclusive: true` fields — making the exact query window unambiguous in output
|
||||
|
||||
**Rationale**: Decayed scoring is time-sensitive by nature. Without a fixed reference point, the same query run minutes apart produces different rankings, making debugging and test reproducibility difficult. `--as-of` pins the clock so that results are deterministic for a given dataset. The upper-bound filter in SQL is critical — without it, events after the as-of date would enter with full weight (since `elapsed.max(0.0)` clamps negative elapsed time to zero), breaking the reproducibility guarantee.
|
||||
|
||||
Implementation: Parse the flag in `run_who()`, compute `as_of_ms: i64`, and thread it through to `query_expert()` where it replaces `now_ms()` and is bound as `?4` in all SQL queries. When the flag is absent, `?4` defaults to `now_ms()` (wall clock), which makes the upper bound transparent — all events are within the window by definition. The flag is compatible with all modes but primarily useful in expert mode.
|
||||
|
||||
### 5b. Score Explainability via `--explain-score`
|
||||
|
||||
Add `--explain-score` flag that augments each expert result with a per-user component breakdown:
|
||||
|
||||
```json
|
||||
{
|
||||
"username": "jsmith",
|
||||
"score": 42,
|
||||
"score_raw": 42.0,
|
||||
"components": {
|
||||
"author": 28.5,
|
||||
"reviewer_participated": 8.2,
|
||||
"reviewer_assigned": 1.8,
|
||||
"notes": 3.5
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
**Scope for this iteration**: Component breakdown only (4 floats per user). No top-evidence MRs, no decay curves, no per-MR drill-down. Those are v2 features if scoring disputes arise frequently.
|
||||
|
||||
**Flag conflicts**: `--explain-score` is mutually exclusive with `--detail`. Both augment per-user output in different ways; combining them would produce confusing overlapping output. Clap `conflicts_with` enforces this at parse time.
|
||||
|
||||
**Human output**: When `--explain-score` is active in human mode, append a parenthetical after each score: `42 (author:28.5 review:10.0 notes:3.5)`.
|
||||
|
||||
**Robot output**: Add `score_raw` (unrounded f64) and `components` object to each expert entry. Only present when `--explain-score` is active (no payload bloat by default). The `resolved_input` section also includes `scoring_model_version: 2` to distinguish from the v1 flat-weight model, enabling robot clients to adapt parsing.
|
||||
|
||||
**Rationale**: Multi-signal decayed ranking will be disputed without decomposition. Showing which signal drives a user's score makes results actionable and builds trust in the model. Keeping scope minimal avoids the output format complexity that originally motivated deferral.
|
||||
|
||||
### 6. Index Migration (db.rs)
|
||||
|
||||
Add a new migration to support the restructured query patterns. The dual-path matching CTEs and `reviewer_participation` CTE introduce query shapes that need index coverage:
|
||||
|
||||
```sql
|
||||
-- Support dual-path matching on DiffNotes (old_path leg of the OR in matched_notes CTE)
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_old_path_author
|
||||
ON notes(position_old_path, author_username, created_at)
|
||||
WHERE note_type = 'DiffNote' AND is_system = 0 AND position_old_path IS NOT NULL;
|
||||
|
||||
-- Support dual-path matching on file changes (old_path leg of the OR in matched_file_changes CTE)
|
||||
CREATE INDEX IF NOT EXISTS idx_mfc_old_path_project_mr
|
||||
ON mr_file_changes(old_path, project_id, merge_request_id)
|
||||
WHERE old_path IS NOT NULL;
|
||||
|
||||
-- Support new_path matching on file changes (ensure index parity with old_path)
|
||||
-- Existing indexes may not have optimal column order for the CTE pattern.
|
||||
CREATE INDEX IF NOT EXISTS idx_mfc_new_path_project_mr
|
||||
ON mr_file_changes(new_path, project_id, merge_request_id);
|
||||
|
||||
-- Support reviewer_participation CTE: joining matched_notes -> discussions -> mr_reviewers
|
||||
-- notes.discussion_id (NOT noteable_id, which doesn't exist in the schema) is the FK to discussions
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_diffnote_discussion_author
|
||||
ON notes(discussion_id, author_username, created_at)
|
||||
WHERE note_type = 'DiffNote' AND is_system = 0;
|
||||
|
||||
-- Support path resolution probes on old_path (build_path_query() and suffix_probe())
|
||||
-- The existing idx_notes_diffnote_path_created covers new_path probes, but old_path probes
|
||||
-- need their own index since probes don't constrain author_username.
|
||||
CREATE INDEX IF NOT EXISTS idx_notes_old_path_project_created
|
||||
ON notes(position_old_path, project_id, created_at)
|
||||
WHERE note_type = 'DiffNote' AND is_system = 0 AND position_old_path IS NOT NULL;
|
||||
```
|
||||
|
||||
**Rationale**: The existing indexes cover `position_new_path` and `new_path` but not their `old_path` counterparts. Without these, the `OR old_path` clauses would force table scans on renamed files. The `reviewer_participation` CTE joins `matched_notes` -> `discussions` -> `merge_requests`, so an index on `(discussion_id, author_username)` speeds up the CTE materialization. The `idx_notes_old_path_project_created` index supports path resolution probes (`build_path_query()` and `suffix_probe()`) which run existence/path-only checks without constraining `author_username` — the scoring-oriented `idx_notes_old_path_author` has `author_username` as the second column, which is suboptimal for these probes.
|
||||
|
||||
**Schema note**: The `notes` table uses `discussion_id` as its FK to `discussions`, which in turn has `merge_request_id`. There is no `noteable_id` column on `notes`. The previous plan revision incorrectly referenced `noteable_id` — this is corrected.
|
||||
|
||||
**Removed**: The `idx_mr_state_timestamps` composite index on `merge_requests(state, merged_at, closed_at, updated_at, created_at)` was removed. MR lookups in the scoring query are always id-driven (joining from `matched_file_changes` or `discussions`), so the state-aware CASE expression operates on rows already fetched by primary key. A low-selectivity composite index on 5 columns would consume space without improving any query path.
|
||||
|
||||
Partial indexes (with `WHERE` clauses) keep the index size minimal — only DiffNote rows and non-null old_path rows are indexed.
|
||||
|
||||
### 7. Test Helpers
|
||||
|
||||
Add timestamp-aware variants:
|
||||
- `insert_mr_at(conn, id, project_id, iid, author, state, updated_at_ms)`
|
||||
- `insert_diffnote_at(conn, id, discussion_id, project_id, author, file_path, body, created_at_ms)`
|
||||
|
||||
### 8. New Tests (TDD)
|
||||
|
||||
#### Example-based tests
|
||||
|
||||
**`test_half_life_decay_math`**: Verify the pure function:
|
||||
- elapsed=0 -> 1.0
|
||||
- elapsed=half_life -> 0.5
|
||||
- elapsed=2*half_life -> 0.25
|
||||
- half_life_days=0 -> 0.0 (guard against div-by-zero)
|
||||
|
||||
**`test_expert_scores_decay_with_time`**: Two authors, one recent (10 days), one old (360 days). Recent author should score ~24, old author ~6.
|
||||
|
||||
**`test_expert_reviewer_decays_faster_than_author`**: Same MR, same age (90 days). Author retains ~18 points, reviewer retains ~5 points. Author dominates clearly.
|
||||
|
||||
**`test_reviewer_participated_vs_assigned_only`**: Two reviewers on the same MR at the same age. One left DiffNotes (participated), one didn't (assigned-only). Participated reviewer should score ~10 * decay, assigned-only should score ~3 * decay. Verifies the split works end-to-end.
|
||||
|
||||
**`test_note_diminishing_returns_per_mr`**: One reviewer with 1 note on MR-A and another with 20 notes on MR-B, both at same age. The 20-note reviewer should score `log2(21)/log2(2) ≈ 4.4x` the 1-note reviewer, NOT 20x. Validates the `log2(1+count)` cap.
|
||||
|
||||
**`test_config_validation_rejects_zero_half_life`**: `ScoringConfig` with `author_half_life_days = 0` should return `ConfigInvalid` error.
|
||||
|
||||
**`test_file_change_timestamp_uses_merged_at`**: An MR with `merged_at` set and `state = 'merged'` should use `merged_at` timestamp, not `updated_at`. Verify by setting `merged_at` to old date and `updated_at` to recent date — score should reflect the old date.
|
||||
|
||||
**`test_open_mr_uses_updated_at`**: An MR with `state = 'opened'` should use `updated_at` (not `created_at`). Verify that an open MR with recent `updated_at` scores higher than one with the same `created_at` but older `updated_at`.
|
||||
|
||||
**`test_old_path_match_credits_expertise`**: Insert a DiffNote with `position_old_path = "src/old.rs"` and `position_new_path = "src/new.rs"`. Query `--path src/old.rs` — the author should appear. Query `--path src/new.rs` — same author should also appear. Validates dual-path matching.
|
||||
|
||||
**`test_explain_score_components_sum_to_total`**: With `--explain-score`, verify that `components.author + components.reviewer_participated + components.reviewer_assigned + components.notes` equals the reported `score_raw` (within f64 rounding tolerance). Note: the closed_mr_multiplier is already folded into the per-component subtotals, not tracked as a separate component.
|
||||
|
||||
**`test_as_of_produces_deterministic_results`**: Insert data at known timestamps. Run `query_expert` twice with the same `--as-of` value — results must be identical. Then run with a later `--as-of` — scores should be lower (more decay).
|
||||
|
||||
**`test_old_path_probe_exact_and_prefix`**: Insert a DiffNote with `position_old_path = "src/old/foo.rs"` and `position_new_path = "src/new/foo.rs"`. Call `build_path_query(conn, "src/old/foo.rs")` — should resolve as exact file (not "not found"). Call `build_path_query(conn, "src/old/")` — should resolve as prefix. Validates that the path resolution probes now check old_path columns.
|
||||
|
||||
**`test_suffix_probe_uses_old_path_sources`**: Insert a file change with `old_path = "legacy/utils.rs"` and `new_path = "src/utils.rs"`. Call `build_path_query(conn, "legacy/utils.rs")` — should resolve via exact probe on old_path. Call `build_path_query(conn, "utils.rs")` — suffix probe should find both `legacy/utils.rs` and `src/utils.rs` and either resolve uniquely (if deduplicated) or report ambiguity.
|
||||
|
||||
**`test_since_relative_to_as_of_clock`**: Insert data at timestamps T1 and T2 (T2 > T1). With `--as-of T2` and `--since 30d`, the window is `[T2 - 30d, T2]`, not `[now - 30d, now]`. Verify that data at T1 is included or excluded based on the as-of-relative window, not the wall clock window.
|
||||
|
||||
**`test_explain_and_detail_are_mutually_exclusive`**: Parsing `--explain-score --detail` should fail with a conflict error from clap.
|
||||
|
||||
**`test_trivial_note_does_not_count_as_participation`**: A reviewer who left only a short note ("LGTM", 4 chars) on an MR should be classified as assigned-only, not participated, when `reviewer_min_note_chars = 20`. A reviewer who left a substantive note (>= 20 chars) should be classified as participated. Validates the LENGTH threshold in the `reviewer_participation` CTE.
|
||||
|
||||
**`test_closed_mr_multiplier`**: Two identical MRs (same author, same age, same path). One is `merged`, one is `closed`. The merged MR should contribute `author_weight * decay(...)`, the closed MR should contribute `author_weight * closed_mr_multiplier * decay(...)`. With default multiplier 0.5, the closed MR contributes half.
|
||||
|
||||
**`test_as_of_excludes_future_events`**: Insert events at timestamps T1 (past) and T2 (future relative to as-of). With `--as-of` set between T1 and T2, only T1 events should appear in results. T2 events must be excluded entirely, not just decayed. Validates the exclusive upper-bound (`< ?4`) filtering in SQL.
|
||||
|
||||
**`test_as_of_exclusive_upper_bound`**: Insert an event with timestamp exactly equal to the `as_of_ms` value. Verify it is excluded from results (strict less-than, not less-than-or-equal). This validates the half-open interval `[since, as_of)` semantics.
|
||||
|
||||
**`test_excluded_usernames_filters_bots`**: Insert signals for a user named "renovate-bot" and a user named "jsmith", both with the same activity. With `excluded_usernames: ["renovate-bot"]` in config, only "jsmith" should appear in results. Validates the Rust-side post-query filtering.
|
||||
|
||||
**`test_include_bots_flag_disables_filtering`**: Same setup as above, but with `--include-bots` active. Both "renovate-bot" and "jsmith" should appear in results.
|
||||
|
||||
**`test_null_timestamp_fallback_to_created_at`**: Insert a merged MR with `merged_at = NULL` (edge case: old data before the column was populated). The state-aware timestamp should fall back to `created_at`. Verify the score reflects `created_at`, not 0 or a panic.
|
||||
|
||||
**`test_path_normalization_handles_dot_and_double_slash`**: Call `normalize_query_path("./src//foo.rs")` — should return `"src/foo.rs"`. Call `normalize_query_path(" src/bar.rs ")` — should return `"src/bar.rs"`. Call `normalize_query_path("src/foo.rs")` — should return unchanged (already normalized). Call `normalize_query_path("")` — should return `""` (empty input passes through).
|
||||
|
||||
**`test_path_normalization_preserves_prefix_semantics`**: Call `normalize_query_path("./src/dir/")` — should return `"src/dir/"` (trailing slash preserved for prefix intent). Call `normalize_query_path("src/dir")` — should return `"src/dir"` (no trailing slash = file, not prefix).
|
||||
|
||||
**`test_config_validation_rejects_absurd_half_life`**: `ScoringConfig` with `author_half_life_days = 5000` (>3650 cap) should return `ConfigInvalid` error. Similarly, `reviewer_min_note_chars = 5000` (>4096 cap) should fail.
|
||||
|
||||
**`test_config_validation_rejects_nan_multiplier`**: `ScoringConfig` with `closed_mr_multiplier = f64::NAN` should return `ConfigInvalid` error. Same for `f64::INFINITY`.
|
||||
|
||||
#### Invariant tests (regression safety for ranking systems)
|
||||
|
||||
**`test_score_monotonicity_by_age`**: For any single signal type, an older timestamp must never produce a higher score than a newer timestamp with the same weight and half-life. Generate N random (age, half_life) pairs and assert `decay(older) <= decay(newer)` for all.
|
||||
|
||||
**`test_row_order_independence`**: Insert the same set of signals in two different orders (e.g., reversed). Run `query_expert` on both — the resulting rankings (username order + scores) must be identical. Validates that neither SQL ordering nor HashMap iteration order affects final output.
|
||||
|
||||
**`test_reviewer_split_is_exhaustive`**: For a reviewer assigned to an MR, they must appear in exactly one of: participated (has substantive DiffNotes meeting `reviewer_min_note_chars`) or assigned-only (no DiffNotes, or only trivial ones below the threshold). Never both, never neither. Test three cases: (1) reviewer with substantive DiffNotes -> participated only, (2) reviewer with no DiffNotes -> assigned-only only, (3) reviewer with only trivial notes ("LGTM") -> assigned-only only.
|
||||
|
||||
**`test_deterministic_accumulation_order`**: Insert signals for a user with contributions at many different timestamps (10+ MRs with varied ages). Run `query_expert` 100 times in a loop. All 100 runs must produce the exact same `f64` score (bit-identical). Validates that the sorted contribution ordering eliminates HashMap-iteration-order nondeterminism.
|
||||
|
||||
### 9. Existing Test Compatibility
|
||||
|
||||
All existing tests insert data with `now_ms()`. With decay, elapsed ~0ms means decay ~1.0, so scores round to the same integers as before. No existing test assertions should break.
|
||||
|
||||
The `test_expert_scoring_weights_are_configurable` test needs `..Default::default()` added to fill the new half-life fields, `reviewer_assignment_weight` / `reviewer_assignment_half_life_days`, `closed_mr_multiplier`, `reviewer_min_note_chars`, and `excluded_usernames` fields.
|
||||
|
||||
## Verification
|
||||
|
||||
1. `cargo check --all-targets` — no compiler errors
|
||||
2. `cargo clippy --all-targets -- -D warnings` — no lints
|
||||
3. `cargo fmt --check` — formatting clean
|
||||
4. `cargo test` — all existing + new tests pass (including invariant tests)
|
||||
5. `ubs src/cli/commands/who.rs src/core/config.rs src/core/db.rs` — no bug scanner findings
|
||||
6. Manual query plan verification (not automated — SQLite planner varies across versions):
|
||||
- Run `EXPLAIN QUERY PLAN` on the expert query (both exact and prefix modes) against a real database
|
||||
- Confirm that `matched_notes_raw` branch 1 uses the existing new_path index and branch 2 uses `idx_notes_old_path_author` (not a full table scan on either branch)
|
||||
- Confirm that `matched_file_changes_raw` branch 1 uses `idx_mfc_new_path_project_mr` and branch 2 uses `idx_mfc_old_path_project_mr`
|
||||
- Confirm that `reviewer_participation` CTE uses `idx_notes_diffnote_discussion_author`
|
||||
- Confirm that `mr_activity` CTE joins `merge_requests` via primary key from `matched_file_changes`
|
||||
- Confirm that path resolution probes (old_path leg) use `idx_notes_old_path_project_created`
|
||||
- Document the observed plan in a comment near the SQL for future regression reference
|
||||
7. Performance baseline (manual, not CI-gated):
|
||||
- Run `time cargo run --release -- who --path <exact-path>` on the real database for exact, prefix, and suffix modes
|
||||
- Target SLOs: p95 exact path < 200ms, prefix < 300ms, suffix < 500ms on development hardware
|
||||
- Record baseline timings as a comment near the SQL for regression reference
|
||||
- If any mode exceeds 2x the baseline after future changes, investigate before merging
|
||||
- Note: These are soft targets for developer awareness, not automated CI gates. Automated benchmarking with synthetic fixtures (100k/1M/5M notes) is a v2 investment if performance becomes a real concern.
|
||||
8. Real-world validation:
|
||||
- `cargo run --release -- who --path MeasurementQualityDialog.tsx` — verify jdefting/zhayes old reviews are properly discounted relative to recent authors
|
||||
- `cargo run --release -- who --path MeasurementQualityDialog.tsx --all-history` — compare full history vs 24m window to validate cutoff is reasonable
|
||||
- `cargo run --release -- who --path MeasurementQualityDialog.tsx --explain-score` — verify component breakdown sums to total and authored signal dominates for known authors
|
||||
- Spot-check that assigned-only reviewers (those who never left DiffNotes) rank below participated reviewers on the same MR
|
||||
- Test a known renamed file path — verify expertise from the old name carries forward
|
||||
- `cargo run --release -- who --path MeasurementQualityDialog.tsx --as-of 2025-06-01` — verify deterministic output across repeated runs
|
||||
- Spot-check that reviewers who only left "LGTM"-style notes are classified as assigned-only (not participated)
|
||||
- Verify closed MRs contribute at ~50% of equivalent merged MR scores via `--explain-score`
|
||||
- If the project has known bot accounts (e.g., renovate-bot), add them to `excluded_usernames` config and verify they no longer appear in results. Run again with `--include-bots` to confirm they reappear.
|
||||
- Test path normalization: `who --path ./src//foo.rs` and `who --path src/foo.rs` should produce identical results
|
||||
|
||||
## Accepted from External Review
|
||||
|
||||
Ideas incorporated from ChatGPT review (feedback-1 through feedback-4) that genuinely improved the plan:
|
||||
|
||||
**From feedback-1 and feedback-2:**
|
||||
- **Path rename awareness (old_path matching)**: Real correctness gap. Both `position_old_path` and `mr_file_changes.old_path` exist in the schema. Simple `OR` clause addition with high value — expertise now survives file renames.
|
||||
- **Hybrid SQL pre-aggregation**: Revised from "fully raw rows" to pre-aggregate note counts per (user, MR) in SQL. MR-level signals were already 1-per-MR; the note rows were the actual scalability risk. Bounded row counts with predictable memory.
|
||||
- **State-aware timestamps**: Improved from our overly-simple `COALESCE(merged_at, created_at)` to a state-aware CASE expression. Open MRs genuinely need `updated_at` to reflect ongoing work; merged MRs need `merged_at` to anchor expertise formation.
|
||||
- **Index migration**: The dual-path matching and CTE patterns need index support. Added partial indexes to keep size minimal.
|
||||
- **Invariant tests**: `test_score_monotonicity_by_age`, `test_row_order_independence`, `test_reviewer_split_is_exhaustive` catch subtle ranking regressions that example-based tests miss.
|
||||
- **`--as-of` flag**: Simple clock-pinning for reproducible decay scoring. Essential for debugging and test determinism.
|
||||
- **`--explain-score` flag**: Moved from rejected to included with minimal scope (component breakdown only, no per-MR drill-down). Multi-signal scoring needs decomposition to build trust.
|
||||
|
||||
**From feedback-3:**
|
||||
- **Fix `noteable_id` index bug (critical)**: The `notes` table uses `discussion_id` as FK to `discussions`, not `noteable_id` (which doesn't exist). The proposed `idx_notes_mr_path_author` index would fail at migration time. Fixed to use `(discussion_id, author_username, created_at)`.
|
||||
- **CTE-based dual-path matching (`matched_notes`, `matched_file_changes`)**: Rather than repeating `OR old_path` in every signal subquery, centralize path matching in foundational CTEs. Defined once, indexed once, maintained once. Cleaner and more extensible.
|
||||
- **Precomputed `reviewer_participation` CTE**: Replaced correlated `EXISTS`/`NOT EXISTS` subqueries with a materialized set of `(mr_id, username)` pairs. Same semantics, lower query cost, simpler reasoning about the reviewer split.
|
||||
- **`log2(1+count)` over `ln(1+count)` for notes**: With `log2`, one note contributes exactly 1.0 unit (since `log2(2) = 1`), making `note_bonus=1` directly interpretable. `ln` gives 0.69 per note, which is unintuitive.
|
||||
- **Path resolution probe rename awareness**: The plan added `old_path` matching to scoring queries but missed the upstream path resolution layer (`build_path_query()` probes and `suffix_probe()`). Without this, querying an old path name fails at resolution and never reaches scoring. Now both probes check old_path columns.
|
||||
- **Removed low-selectivity `idx_mr_state_timestamps`**: MR lookups in scoring are id-driven (from file_changes or discussions), so a 5-column composite on state/timestamps adds no query benefit.
|
||||
- **Added `idx_mfc_new_path_project_mr`**: Ensures index parity between old and new path columns on `mr_file_changes`.
|
||||
- **`--explain-score` conflicts with `--detail`**: Prevents confusing overlapping output from two per-user augmentation flags.
|
||||
- **`scoring_model_version` in resolved_input**: Lets robot clients distinguish v1 (flat weights) from v2 (decayed) output schemas.
|
||||
- **`score_raw` in explain mode**: Exposes the unrounded f64 so component totals can be verified without rounding noise.
|
||||
- **New tests**: `test_old_path_probe_exact_and_prefix`, `test_suffix_probe_uses_old_path_sources`, `test_since_relative_to_as_of_clock`, `test_explain_and_detail_are_mutually_exclusive`, `test_null_timestamp_fallback_to_created_at` — cover the newly-identified gaps in path resolution, clock semantics, and edge cases.
|
||||
- **EXPLAIN QUERY PLAN verification step**: Manual check that the restructured queries use the new indexes (not automated, since SQLite planner varies across versions).
|
||||
|
||||
**From feedback-4:**
|
||||
- **`--as-of` temporal correctness (critical)**: The plan described `--as-of` but the SQL only enforced a lower bound (`>= ?2`). Events after the as-of date would leak in with full weight (because `elapsed.max(0.0)` clamps negative elapsed time to zero). Added `< ?4` upper bound to all SQL timestamp filters, making the query window `[since_ms, as_of_ms)`. Without this, `--as-of` reproducibility was fundamentally broken. (Refined to exclusive upper bound in feedback-5.)
|
||||
- **Closed-state inconsistency resolution**: The state-aware CASE expression handled `closed` state but the WHERE clause filtered to `('opened','merged')` only — dead code. Resolved by including `'closed'` in state filters and adding a `closed_mr_multiplier` (default 0.5) applied in Rust to all signals from closed-without-merge MRs. This credits real review effort on abandoned MRs while appropriately discounting it.
|
||||
- **Substantive note threshold for reviewer participation**: A single "LGTM" shouldn't promote a reviewer from 3-point (assigned-only) to 10-point (participated) weight. Added `reviewer_min_note_chars` (default 20) config field and `LENGTH(TRIM(body))` filter in the `reviewer_participation` CTE. This raises the bar for participation classification to actual substantive review comments.
|
||||
- **UNION ALL optimization for path predicates**: SQLite's planner can degrade `OR` across two indexed columns to a table scan. Originally documented as a fallback; promoted to default strategy in feedback-5 iteration. The UNION ALL + dedup approach ensures each index branch is used independently.
|
||||
- **New tests**: `test_trivial_note_does_not_count_as_participation`, `test_closed_mr_multiplier`, `test_as_of_excludes_future_events` — cover the three new features added from this review round.
|
||||
|
||||
**From feedback-5 (ChatGPT review):**
|
||||
- **Exclusive upper bound for `--as-of`**: Changed from `[since_ms, as_of_ms]` (inclusive) to `[since_ms, as_of_ms)` (exclusive). Half-open intervals are the standard convention in temporal systems — they eliminate edge-case ambiguity when events have timestamps exactly at the boundary. Also added `YYYY-MM-DD` → end-of-day UTC parsing and window metadata in robot output.
|
||||
- **UNION ALL as default for dual-path matching**: Promoted from "fallback if planner regresses" to default strategy. SQLite `OR`-across-indexed-columns degradation is common enough that the predictable UNION ALL + dedup approach is the safer starting point. The simpler `OR` variant is retained as a comment for benchmarking.
|
||||
- **Deterministic contribution ordering**: Within each signal type, sort contributions by `mr_id` before summing. This eliminates HashMap iteration order as a source of f64 rounding variance near ties, ensuring CI reproducibility without the overhead of compensated summation (Neumaier/Kahan was rejected as overkill at this scale).
|
||||
- **Minimal bot/service-account filtering**: Added `excluded_usernames` (exact match, case-insensitive) to `ScoringConfig` and `--include-bots` CLI flag. Applied as a Rust-side post-filter (not SQL) to keep queries clean. Scope is deliberately minimal — no regex patterns, no heuristic detection. Users configure the list for their team's specific bots.
|
||||
- **Performance baseline SLOs**: Added manual performance baseline step to verification — record timings for exact/prefix/suffix modes and flag >2x regressions. Kept lightweight (no CI gating, no synthetic benchmarks) to match the project's current maturity.
|
||||
- **New tests**: `test_as_of_exclusive_upper_bound`, `test_excluded_usernames_filters_bots`, `test_include_bots_flag_disables_filtering`, `test_deterministic_accumulation_order` — cover the newly-accepted features.
|
||||
|
||||
**From feedback-6 (ChatGPT review):**
|
||||
- **Centralized `mr_activity` CTE**: The state-aware timestamp CASE expression and `closed_mr_multiplier` were repeated across signals 3, 4a, 4b with the multiplier applied later in Rust via string-matching on `mr_state`. This was brittle — the CASE could drift between branches and the Rust-side string matching was unnecessary indirection. A single `mr_activity` CTE defines both `activity_ts` and `state_mult` once, scoped to matched MRs only (via JOIN with `matched_file_changes`). Signals 1 and 2 still compute `state_mult` inline because they join through `discussions`, not `matched_file_changes`.
|
||||
- **Parameterized `reviewer_min_note_chars` and `closed_mr_multiplier`**: Previously `reviewer_min_note_chars` was inlined as a literal in the SQL string and `closed_mr_multiplier` was applied only in Rust. Binding both as SQL parameters (`?5` for `closed_mr_multiplier`, `?6` for `reviewer_min_note_chars`) eliminates statement-cache churn, ensures identical SQL text regardless of config values, and simplifies EXPLAIN QUERY PLAN analysis.
|
||||
- **Tightened config validation**: Added upper bounds — `*_half_life_days <= 3650` (10-year safety cap), `reviewer_min_note_chars <= 4096`, and `closed_mr_multiplier` must be finite (not NaN/Inf). These prevent absurd configurations from silently producing nonsensical results.
|
||||
- **Path canonicalization via `normalize_query_path()`**: Inputs like `./src//foo.rs` or whitespace-padded paths could fail path resolution even when the file exists in the database. A simple syntactic normalization (strip `./`, collapse `//`, trim whitespace, preserve trailing `/`) runs before `build_path_query()` to reduce false negatives. No filesystem or database lookups — purely string manipulation.
|
||||
- **Probe-optimized `idx_notes_old_path_project_created` index**: The scoring-oriented `idx_notes_old_path_author` index has `author_username` as its second column, which is suboptimal for path resolution probes that don't constrain author. A dedicated probe index on `(position_old_path, project_id, created_at)` ensures `build_path_query()` and `suffix_probe()` old_path lookups are efficient.
|
||||
- **New tests**: `test_path_normalization_handles_dot_and_double_slash`, `test_path_normalization_preserves_prefix_semantics`, `test_config_validation_rejects_absurd_half_life`, `test_config_validation_rejects_nan_multiplier` — cover the path canonicalization and tightened validation logic.
|
||||
|
||||
## Rejected Ideas (with rationale)
|
||||
|
||||
These suggestions were considered during review but explicitly excluded from this iteration:
|
||||
|
||||
- **Rename alias chain expansion (A->B->C traversal)** (feedback-2 #2, feedback-4 #4): Over-engineered for v1. The old_path `OR` match covers the 80% case (direct renames). Building a canonical path identity table at ingest time adds schema, ingestion logic, and graph traversal complexity for rare multi-hop renames. If real-world usage shows fragmented expertise on multi-rename files, this becomes a v2 feature.
|
||||
- **Config-driven `max_age_days`** (feedback-1 #5, feedback-2 #5): We already have `--since` (explicit window), `--all-history` (no window), and the 24m default (mathematically justified). Adding a config field that derives the default since window creates confusing interaction between config and CLI flags. If half-lives change, updating the default constant is trivial.
|
||||
- **Config-driven `decay_floor` for derived `--since` default** (feedback-3 #4): Proposed computing the default since window as `ceil(max_half_life * log2(1/floor))` so it auto-adjusts when half-lives change. Rejected: the formula is non-obvious to users, adds a config param (`decay_floor`) with no intuitive meaning, and the benefit is negligible — half-life changes are rare, and updating a constant is trivial. The 24m default is already mathematically justified and easy to override with `--since` or `--all-history`.
|
||||
- **BTreeMap + Kahan/Neumaier compensated summation** (feedback-3 #6): Proposed deterministic iteration order and numerically stable summation. Rejected for this scale: the accumulator processes dozens to low hundreds of entries per user, where HashMap iteration order doesn't measurably affect f64 sums. Compensated summation adds code complexity for zero practical benefit at this magnitude. If we eventually aggregate thousands of signals per user, revisit.
|
||||
- **Confidence/coverage metadata** (feedback-1 #8, feedback-2 #8, feedback-3 #9, feedback-4 #6): Repeatedly proposed across reviews with variations (score_adjusted with confidence factor, low/medium/high labels, evidence_mr_count weighting). Still scope creep. The `--explain-score` component breakdown already tells users which signal drives the score. Defining "sparse evidence" thresholds (how many MRs is "low"? what's the right exponential saturation constant?) is domain-specific guesswork without user feedback data. A single recent MR "outranking broader expertise" is the *correct* behavior of time-decay — the model intentionally weights recency. If real-world usage shows this is a problem, confidence becomes a v2 feature informed by actual threshold data.
|
||||
- **Automated EXPLAIN QUERY PLAN tests** (feedback-3 #10 partial): SQLite's query planner changes across versions and can use different plans on different data distributions. Automated assertions on plan output are brittle. Instead, we document EXPLAIN QUERY PLAN as a manual verification step during development and include the observed plan as a comment near the SQL.
|
||||
- **Per-MR evidence drill-down in `--explain-score`** (feedback-2 #7 promoted this): The v1 `--explain-score` shows component totals only. Listing top-evidence MRs per user would require additional SQL queries and significant output format work. Deferred unless component breakdowns prove insufficient for debugging.
|
||||
- **Split scoring engine into core module** (feedback-4 #5): Proposed extracting scoring math from `who.rs` into `src/core/scoring/model_v2_decay.rs`. Premature modularization — `who.rs` is the only consumer and is ~800 lines. Adding module plumbing and indirection for a single call site adds complexity without reducing it. If we add a second scoring consumer (e.g., automated triage), revisit.
|
||||
- **Bot/service-account filtering** (feedback-4 #7): Real concern but orthogonal to time-decay scoring. This is a general data quality feature that belongs in its own issue — it affects all `who` modes, not just expert scoring. Adding `excluded_username_patterns` config and `--include-bots` flag is scope expansion that should be designed and tested independently.
|
||||
- **Model compare mode / rank-delta diagnostics** (feedback-4 #9): Over-engineered rollout safety for an internal CLI tool with ~3 users. Maintaining two parallel scoring codepaths (v1 flat + v2 decayed) doubles test surface and code complexity. The `--explain-score` + `--as-of` combination already provides debugging capability. If a future model change is risky enough to warrant A/B comparison, build it then.
|
||||
- **Canonical path identity graph** (feedback-5 #1, also feedback-2 #2, feedback-4 #4): Third time proposed, third time rejected. Building a rename graph from `mr_file_changes(old_path, new_path)` with identity resolution requires new schema (`path_identities`, `path_aliases` tables), ingestion pipeline changes, graph traversal at query time, and backfill logic for existing data. The UNION ALL dual-path matching already covers the 80%+ case (direct renames). Multi-hop rename chains (A→B→C) are rare in practice and can be addressed in v2 with real usage data showing the gap matters.
|
||||
- **Normalized `expertise_events` table** (feedback-5 #2): Proposes shifting from query-time CTE joins to a precomputed `expertise_events` table populated at ingest time. While architecturally appealing for read performance, this doubles the data surface area (raw tables + derived events), requires new ingestion pipelines with incremental upsert logic, backfill tooling for existing databases, and introduces consistency risks when raw data is corrected/re-synced. The CTE approach is correct, maintainable, and performant at our current scale. If query latency becomes a real bottleneck (see performance baseline SLOs), materialized views or derived tables become a v2 optimization.
|
||||
- **Reviewer engagement model upgrade** (feedback-5 #3): Proposes adding `approved`/`changes_requested` review-state signals and trivial-comment pattern matching (`["lgtm","+1","nit","ship it"]`). Expands the signal type count from 4 to 6 and adds a fragile pattern-matching layer (what about "don't ship it"? "lgtm but..."?). The `reviewer_min_note_chars` threshold is imperfect but pragmatic — it's a single configurable number with no false-positive risk from substring matching. Review-state signals may be worth adding later as a separate enhancement when we have data on how often they diverge from DiffNote participation.
|
||||
- **Contribution-floor auto cutoff for `--since`** (feedback-5 #5): Proposes `--since auto` computing the earliest relevant timestamp from `min_contribution_floor` (e.g., 0.01 points). Adds a non-obvious config parameter for minimal benefit — the 24m default is already mathematically justified from the decay curves (author: 6%, reviewer: 0.4% at 2 years) and easily overridden with `--since` or `--all-history`. The auto-derivation formula (`ceil(max_half_life * log2(1/floor))`) is opaque to users who just want to understand why a certain time range was selected.
|
||||
- **Full evidence drill-down in `--explain-score`** (feedback-5 #8): Proposes `--explain-score=summary|full` with per-MR evidence rows. Already rejected in feedback-2 #7. Component totals are sufficient for v1 debugging — they answer "which signal type drives this user's score." Per-MR drill-down requires additional SQL queries and significant output format complexity. Deferred unless component breakdowns prove insufficient.
|
||||
- **Neumaier compensated summation** (feedback-5 #7 partial): Accepted the sorting aspect for deterministic ordering, but rejected Neumaier/Kahan compensated summation. At the scale of dozens to low hundreds of contributions per user, the rounding error from naive f64 summation is on the order of 1e-14 — several orders of magnitude below any meaningful score difference. Compensated summation adds code complexity and a maintenance burden for no practical benefit at this scale.
|
||||
- **Automated CI benchmark gate** (feedback-5 #10 partial): Accepted manual performance baselines, but rejected automated CI regression gating with synthetic fixtures (100k/1M/5M notes). Building and maintaining benchmark infrastructure is a significant investment that's premature for a CLI tool with ~3 users. Manual timing checks during development are sufficient until performance becomes a real concern.
|
||||
- **Epsilon-based tie buckets for ranking** (feedback-6 #4) — rejected because the plan already has deterministic contribution ordering by `mr_id` within each signal type, which eliminates HashMap-iteration nondeterminism. Platform-dependent `powf` differences at the scale of dozens to hundreds of contributions per user are sub-epsilon (order of 1e-15). If two users genuinely score within 1e-9 of each other, the existing tiebreak by `(last_seen DESC, username ASC)` is already meaningful and deterministic. Adding a bucketing layer introduces a magic epsilon constant and floor operation for a problem that doesn't manifest in practice.
|
||||
- **`--diagnose-score` aggregated diagnostics flag** (feedback-6 #5) — rejected because this is diagnostic/debugging tooling that adds a new flag, new output format, and new counting logic (matched_notes_raw_count, dedup_count, window exclusions, etc.) across the SQL pipeline. The existing `--explain-score` component breakdown + manual EXPLAIN QUERY PLAN verification already covers the debugging need. The additional SQL instrumentation required (counting rows at each CTE stage) would complicate the query for a feature with unclear demand. A v2 addition if operational debugging becomes a recurring need.
|
||||
- **Multi-path expert scoring (`--path` repeatable)** (feedback-6 #7) — rejected because this is a feature expansion, not a plan improvement for the time-decay model. Multi-path requires a `requested_paths` CTE, modified dedup logic keyed on `(username, signal, mr_id)` across paths, CLI parsing changes for repeatable `--path` and `--path-file`, and new test cases for overlap/prefix/dedup semantics. This is a separate bead/feature that should be designed independently — it's orthogonal to time-decay scoring and can be added later without requiring any changes to the decay model.
|
||||
209
plans/tui-prd-v2-frankentui.feedback-1.md
Normal file
209
plans/tui-prd-v2-frankentui.feedback-1.md
Normal file
@@ -0,0 +1,209 @@
|
||||
No `## Rejected Recommendations` section was present, so these are all net-new improvements.
|
||||
|
||||
1. Keep core `lore` stable; isolate nightly to a TUI crate
|
||||
Rationale: the current plan says “whole project nightly” but later assumes TUI is feature-gated. Isolating nightly removes unnecessary risk from non-TUI users, CI, and release cadence.
|
||||
|
||||
```diff
|
||||
@@ 3.2 Nightly Rust Strategy
|
||||
-- The entire gitlore project moves to pinned nightly, not just the TUI feature.
|
||||
+- Keep core `lore` on stable Rust.
|
||||
+- Add workspace member `lore-tui` pinned to nightly for FrankenTUI.
|
||||
+- Ship `lore tui` only when `--features tui` (or separate `lore-tui` binary) is enabled.
|
||||
|
||||
@@ 10.1 New Files
|
||||
+- crates/lore-tui/Cargo.toml
|
||||
+- crates/lore-tui/src/main.rs
|
||||
|
||||
@@ 11. Assumptions
|
||||
-17. TUI module is feature-gated.
|
||||
+17. TUI is isolated in a workspace crate and feature-gated in root CLI integration.
|
||||
```
|
||||
|
||||
2. Add a framework adapter boundary from day 1
|
||||
Rationale: the “3-day ratatui escape hatch” is optimistic without a strict interface. A tiny `UiRuntime` + screen renderer trait makes fallback real, not aspirational.
|
||||
|
||||
```diff
|
||||
@@ 4. Architecture
|
||||
+### 4.9 UI Runtime Abstraction
|
||||
+Introduce `UiRuntime` trait (`run`, `send`, `subscribe`) and `ScreenRenderer` trait.
|
||||
+FrankenTUI implementation is default; ratatui adapter can be dropped in with no state/action rewrite.
|
||||
|
||||
@@ 3.5 Escape Hatch
|
||||
-- The migration cost to ratatui is ~3 days
|
||||
+- Migration cost target is ~3-5 days, validated by one ratatui spike screen in Phase 1.
|
||||
```
|
||||
|
||||
3. Stop using CLI command modules as the TUI query API
|
||||
Rationale: coupling TUI to CLI output-era structs creates long-term friction and accidental regressions. Create a shared domain query layer used by both CLI and TUI.
|
||||
|
||||
```diff
|
||||
@@ 10.20 Refactor: Extract Query Functions
|
||||
-- extract query_* from cli/commands/*
|
||||
+- introduce `src/domain/query/*` as the canonical read model API.
|
||||
+- CLI and TUI both depend on domain query layer.
|
||||
+- CLI modules retain formatting/output only.
|
||||
|
||||
@@ 10.2 Modified Files
|
||||
+- src/domain/query/mod.rs
|
||||
+- src/domain/query/issues.rs
|
||||
+- src/domain/query/mrs.rs
|
||||
+- src/domain/query/search.rs
|
||||
+- src/domain/query/who.rs
|
||||
```
|
||||
|
||||
4. Replace single `Arc<Mutex<Connection>>` with connection manager
|
||||
Rationale: one locked connection serializes everything and hurts responsiveness, especially during sync. Use separate read pool + writer connection with WAL and busy timeout.
|
||||
|
||||
```diff
|
||||
@@ 4.4 App — Implementing the Model Trait
|
||||
- pub db: Arc<Mutex<Connection>>,
|
||||
+ pub db: Arc<DbManager>, // read pool + single writer coordination
|
||||
|
||||
@@ 4.5 Async Action System
|
||||
- Each Cmd::task closure locks the mutex, runs the query, and returns a Msg
|
||||
+ Reads use pooled read-only connections.
|
||||
+ Sync/write path uses dedicated writer connection.
|
||||
+ Enforce WAL, busy_timeout, and retry policy for SQLITE_BUSY.
|
||||
```
|
||||
|
||||
5. Make debouncing/cancellation explicit and correct
|
||||
Rationale: “runtime coalesces rapid keypresses” is not a safe correctness guarantee. Add request IDs and stale-response dropping to prevent flicker and wrong data.
|
||||
|
||||
```diff
|
||||
@@ 4.3 Core Types (Msg)
|
||||
+ SearchRequestStarted { request_id: u64, query: String }
|
||||
- SearchExecuted(SearchResults),
|
||||
+ SearchExecuted { request_id: u64, results: SearchResults },
|
||||
|
||||
@@ 4.4 maybe_debounced_query()
|
||||
- runtime coalesces rapid keypresses
|
||||
+ use explicit 200ms debounce timer + monotonic request_id
|
||||
+ ignore results whose request_id != current_search_request_id
|
||||
```
|
||||
|
||||
6. Implement true streaming sync, not batch-at-end pseudo-streaming
|
||||
Rationale: the plan promises real-time logs/progress but code currently returns one completion message. This gap will disappoint users and complicate cancellation.
|
||||
|
||||
```diff
|
||||
@@ 4.4 start_sync_task()
|
||||
- Pragmatic approach: run sync synchronously, collect all progress events, return summary.
|
||||
+ Use event channel subscription for `SyncProgress`/`SyncLogLine` streaming.
|
||||
+ Keep `SyncCompleted` only as terminal event.
|
||||
+ Add cooperative cancel token mapped to `Esc` while running.
|
||||
|
||||
@@ 5.9 Sync
|
||||
+ Add "Resume from checkpoint" option for interrupted syncs.
|
||||
```
|
||||
|
||||
7. Fix entity identity ambiguity across projects
|
||||
Rationale: using `iid` alone is unsafe in multi-project datasets. Navigation and cross-refs should key by `(project_id, iid)` or global ID.
|
||||
|
||||
```diff
|
||||
@@ 4.3 Core Types
|
||||
- IssueDetail(i64)
|
||||
- MrDetail(i64)
|
||||
+ IssueDetail(EntityKey)
|
||||
+ MrDetail(EntityKey)
|
||||
|
||||
+ pub struct EntityKey { pub project_id: i64, pub iid: i64, pub kind: EntityKind }
|
||||
|
||||
@@ 10.12.4 Cross-Reference Widget
|
||||
- parse "group/project#123" -> iid only
|
||||
+ parse into `{project_path, iid, kind}` then resolve to `project_id` before navigation
|
||||
```
|
||||
|
||||
8. Resolve keybinding conflicts and formalize keymap precedence
|
||||
Rationale: current spec conflicts (`Tab` sort vs focus filter; `gg` vs go-prefix). A deterministic keymap contract prevents UX bugs.
|
||||
|
||||
```diff
|
||||
@@ 8.2 List Screens
|
||||
- Tab | Cycle sort column
|
||||
- f | Focus filter bar
|
||||
+ Tab | Focus filter bar
|
||||
+ S | Cycle sort column
|
||||
+ / | Focus filter bar (alias)
|
||||
|
||||
@@ 4.4 interpret_key()
|
||||
+ Add explicit precedence table:
|
||||
+ 1) modal/palette
|
||||
+ 2) focused input
|
||||
+ 3) global
|
||||
+ 4) screen-local
|
||||
+ Add configurable go-prefix timeout (default 500ms) with cancel feedback.
|
||||
```
|
||||
|
||||
9. Add performance SLOs and DB/index plan
|
||||
Rationale: “fast enough” is vague. Add measurable budgets, required indexes, and query-plan gates in CI for predictable performance.
|
||||
|
||||
```diff
|
||||
@@ 3.1 Risk Matrix
|
||||
+ Add risk: "Query latency regressions on large datasets"
|
||||
|
||||
@@ 9.3 Phase 0 — Toolchain Gate
|
||||
+7. p95 list query latency < 75ms on 100k issues synthetic fixture
|
||||
+8. p95 search latency < 200ms on 1M docs (lexical mode)
|
||||
|
||||
@@ 11. Assumptions
|
||||
-5. SQLite queries are fast enough for interactive use (<50ms for filtered results).
|
||||
+5. Performance budgets are enforced by benchmark fixtures and query-plan checks.
|
||||
+6. Required indexes documented and migration-backed before TUI GA.
|
||||
```
|
||||
|
||||
10. Add reliability/observability model (error classes, retries, tracing)
|
||||
Rationale: one string toast is not enough for production debugging. Add typed errors, retry policy, and an in-TUI diagnostics pane.
|
||||
|
||||
```diff
|
||||
@@ 4.3 Core Types (Msg)
|
||||
- Error(String),
|
||||
+ Error(AppError),
|
||||
|
||||
+ pub enum AppError {
|
||||
+ DbBusy, DbCorruption, NetworkRateLimited, NetworkUnavailable,
|
||||
+ AuthFailed, ParseError, Internal(String)
|
||||
+ }
|
||||
|
||||
@@ 5.11 Doctor / Stats
|
||||
+ Add "Diagnostics" tab:
|
||||
+ - last 100 errors
|
||||
+ - retry counts
|
||||
+ - current sync/backoff state
|
||||
+ - DB contention metrics
|
||||
```
|
||||
|
||||
11. Add “Saved Views + Watchlist” as high-value product features
|
||||
Rationale: this makes the TUI compelling daily, not just navigable. Users can persist filters and monitor critical slices (e.g., “P1 auth issues updated in last 24h”).
|
||||
|
||||
```diff
|
||||
@@ 1. Executive Summary
|
||||
+ - Saved Views (named filters and layouts)
|
||||
+ - Watchlist panel (tracked queries with delta badges)
|
||||
|
||||
@@ 5. Screen Taxonomy
|
||||
+### 5.12 Saved Views / Watchlist
|
||||
+Persistent named filters for Issues/MRs/Search.
|
||||
+Dashboard shows per-watchlist deltas since last session.
|
||||
|
||||
@@ 6. User Flows
|
||||
+### 6.9 Flow: "Run morning watchlist triage"
|
||||
+Dashboard -> Watchlist -> filtered IssueList/MRList -> detail drilldown
|
||||
```
|
||||
|
||||
12. Strengthen testing plan with deterministic behavior and chaos cases
|
||||
Rationale: snapshot tests alone won’t catch race/staleness/cancellation issues. Add concurrency, cancellation, and flaky terminal behavior tests.
|
||||
|
||||
```diff
|
||||
@@ 9.2 Phases
|
||||
+Phase 5.5 Reliability Test Pack (2d)
|
||||
+ - stale response drop tests
|
||||
+ - sync cancel/resume tests
|
||||
+ - SQLITE_BUSY retry tests
|
||||
+ - resize storm and rapid key-chord tests
|
||||
|
||||
@@ 10.9 Snapshot Test Example
|
||||
+ Add non-snapshot tests:
|
||||
+ - property tests for navigation invariants
|
||||
+ - integration tests for request ordering correctness
|
||||
+ - benchmark tests for query budgets
|
||||
```
|
||||
|
||||
If you want, I can produce a consolidated “PRD v2.1 patch” with all of the above merged into one coherent updated document structure.
|
||||
214
plans/tui-prd-v2-frankentui.feedback-10.md
Normal file
214
plans/tui-prd-v2-frankentui.feedback-10.md
Normal file
@@ -0,0 +1,214 @@
|
||||
I found 9 high-impact revisions that materially improve correctness, robustness, and usability without reintroducing anything in `## Rejected Recommendations`.
|
||||
|
||||
### 1. Prevent stale async overwrites on **all** screens (not just search)
|
||||
Right now, only `SearchExecuted` is generation-guarded. `IssueListLoaded`, `MrListLoaded`, `IssueDetailLoaded`, etc. can still race and overwrite newer state after rapid navigation/filtering. This is the biggest correctness risk in the current design.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ message.rs
|
||||
- IssueListLoaded(Vec<IssueRow>),
|
||||
+ IssueListLoaded { generation: u64, rows: Vec<IssueRow> },
|
||||
@@
|
||||
- MrListLoaded(Vec<MrRow>),
|
||||
+ MrListLoaded { generation: u64, rows: Vec<MrRow> },
|
||||
@@
|
||||
- IssueDetailLoaded { key: EntityKey, detail: IssueDetail },
|
||||
- MrDetailLoaded { key: EntityKey, detail: MrDetail },
|
||||
+ IssueDetailLoaded { generation: u64, key: EntityKey, detail: IssueDetail },
|
||||
+ MrDetailLoaded { generation: u64, key: EntityKey, detail: MrDetail },
|
||||
|
||||
@@ update()
|
||||
- Msg::IssueListLoaded(result) => {
|
||||
+ Msg::IssueListLoaded { generation, rows } => {
|
||||
+ if !self.task_supervisor.is_current(&TaskKey::LoadScreen(Screen::IssueList), generation) {
|
||||
+ return Cmd::none();
|
||||
+ }
|
||||
self.state.set_loading(false);
|
||||
- self.state.issue_list.set_result(result);
|
||||
+ self.state.issue_list.set_result(rows);
|
||||
Cmd::none()
|
||||
}
|
||||
```
|
||||
|
||||
### 2. Make cancellation safe with task-owned SQLite interrupt handles
|
||||
The plan mentions `sqlite3_interrupt()` but uses pooled shared reader connections. Interrupting a shared connection can cancel unrelated work. Use per-task reader leases and store `InterruptHandle` in `TaskHandle`.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ DbManager
|
||||
- readers: Vec<Mutex<Connection>>,
|
||||
+ readers: Vec<Mutex<Connection>>,
|
||||
+ // task-scoped interrupt handles prevent cross-task cancellation bleed
|
||||
+ // each dispatched query receives an owned ReaderLease
|
||||
|
||||
+pub struct ReaderLease {
|
||||
+ conn: Connection,
|
||||
+ interrupt: rusqlite::InterruptHandle,
|
||||
+}
|
||||
+
|
||||
+impl DbManager {
|
||||
+ pub fn lease_reader(&self) -> Result<ReaderLease, LoreError> { ... }
|
||||
+}
|
||||
|
||||
@@ TaskHandle
|
||||
pub struct TaskHandle {
|
||||
pub key: TaskKey,
|
||||
pub generation: u64,
|
||||
pub cancel: Arc<CancelToken>,
|
||||
+ pub interrupt: Option<rusqlite::InterruptHandle>,
|
||||
}
|
||||
|
||||
@@ cancellation
|
||||
-Query interruption: ... fires sqlite3_interrupt() on the connection.
|
||||
+Query interruption: cancel triggers the task's owned InterruptHandle only.
|
||||
+No shared-connection interrupt is permitted.
|
||||
```
|
||||
|
||||
### 3. Harden keyset pagination for multi-project and sort changes
|
||||
`updated_at + iid` cursor is not enough when rows share timestamps across projects or sort mode changes. This can duplicate/skip rows.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ issue_list.rs
|
||||
-pub struct IssueCursor {
|
||||
- pub updated_at: i64,
|
||||
- pub iid: i64,
|
||||
-}
|
||||
+pub struct IssueCursor {
|
||||
+ pub sort_field: SortField,
|
||||
+ pub sort_order: SortOrder,
|
||||
+ pub updated_at: Option<i64>,
|
||||
+ pub created_at: Option<i64>,
|
||||
+ pub iid: i64,
|
||||
+ pub project_id: i64, // deterministic tie-breaker
|
||||
+ pub filter_hash: u64, // invalidates stale cursors on filter mutation
|
||||
+}
|
||||
|
||||
@@ pagination section
|
||||
-Windowed keyset pagination ...
|
||||
+Windowed keyset pagination uses deterministic tuple ordering:
|
||||
+`ORDER BY <primary_sort>, project_id, iid`.
|
||||
+Cursor is rejected if `filter_hash` or sort tuple mismatches current query.
|
||||
```
|
||||
|
||||
### 4. Replace ad-hoc filter parsing with a small typed DSL
|
||||
Current `split_whitespace()` parser is brittle and silently lossy. Add quoted values, negation, and strict parse errors.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ filter_bar.rs
|
||||
- fn parse_tokens(&mut self) {
|
||||
- let text = self.input.value().to_string();
|
||||
- self.tokens = text.split_whitespace().map(|chunk| { ... }).collect();
|
||||
- }
|
||||
+ fn parse_tokens(&mut self) {
|
||||
+ // grammar (v1):
|
||||
+ // term := [ "-" ] (field ":" value | quoted_text | bare_text)
|
||||
+ // value := quoted | unquoted
|
||||
+ // examples:
|
||||
+ // state:opened label:"P1 blocker" -author:bot since:14d
|
||||
+ self.tokens = filter_dsl::parse(self.input.value())?;
|
||||
+ }
|
||||
|
||||
@@ section 8 / keybindings-help
|
||||
+Filter parser surfaces actionable inline diagnostics with cursor position,
|
||||
+and never silently drops unknown fields.
|
||||
```
|
||||
|
||||
### 5. Add render caches for markdown/tree shaping
|
||||
Markdown and tree shaping are currently recomputed on every frame in several snippets. Cache render artifacts by `(entity, width, theme, content_hash)` to protect frame time.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ module structure
|
||||
+ render_cache.rs # Width/theme/content-hash keyed cache for markdown + tree layouts
|
||||
|
||||
@@ Assumptions / Performance
|
||||
+Detail and search preview rendering uses memoized render artifacts.
|
||||
+Cache invalidation triggers: content hash change, terminal width change, theme change.
|
||||
```
|
||||
|
||||
### 6. Use one-shot timers for debounce/prefix timeout
|
||||
`Every` is periodic; it wakes repeatedly and can produce edge-case repeated firings. One-shot subscriptions are cleaner and cheaper.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ subscriptions()
|
||||
- if self.state.search.debounce_pending() {
|
||||
- subs.push(Box::new(
|
||||
- Every::with_id(3, Duration::from_millis(200), move || {
|
||||
- Msg::SearchDebounceFired { generation }
|
||||
- })
|
||||
- ));
|
||||
- }
|
||||
+ if self.state.search.debounce_pending() {
|
||||
+ subs.push(Box::new(
|
||||
+ After::with_id(3, Duration::from_millis(200), move || {
|
||||
+ Msg::SearchDebounceFired { generation }
|
||||
+ })
|
||||
+ ));
|
||||
+ }
|
||||
|
||||
@@ InputMode GoPrefix timeout
|
||||
-The tick subscription compares clock instant...
|
||||
+GoPrefix timeout is a one-shot `After(500ms)` tied to prefix generation.
|
||||
```
|
||||
|
||||
### 7. New feature: list “Quick Peek” panel (`Space`) for triage speed
|
||||
This adds immediate value without v2-level scope. Users can inspect selected issue/MR metadata/snippet without entering detail and coming back.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 5.2 Issue List
|
||||
-Interaction: Enter detail
|
||||
+Interaction: Enter detail, Space quick-peek (toggle right preview pane)
|
||||
|
||||
@@ 5.4 MR List
|
||||
+Quick Peek mode mirrors Issue List: metadata + first discussion snippet + cross-refs.
|
||||
|
||||
@@ 8.2 List Screens
|
||||
| `Enter` | Open selected item |
|
||||
+| `Space` | Toggle Quick Peek panel for selected row |
|
||||
```
|
||||
|
||||
### 8. Upgrade compatibility handshake from integer to machine-readable contract
|
||||
Single integer compat is too coarse for real drift detection. Keep it simple but structured.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ Nightly Rust Strategy / Compatibility contract
|
||||
- 1. Binary compat version (`lore-tui --compat-version`) — integer check ...
|
||||
+ 1. Binary compat contract (`lore-tui --compat-json`) — JSON:
|
||||
+ `{ "protocol": 1, "compat_version": 2, "min_schema": 14, "max_schema": 16, "build": "..." }`
|
||||
+ `lore` validates protocol + compat + schema range before spawn.
|
||||
|
||||
@@ CLI integration
|
||||
-fn validate_tui_compat(...) { ... --compat-version ... }
|
||||
+fn validate_tui_compat(...) { ... --compat-json ... }
|
||||
```
|
||||
|
||||
### 9. Fix sync stream bug and formalize progress coalescing
|
||||
The current snippet calls `try_send` for progress twice in one callback path and depth math is wrong. Also progress spam should be coalesced by lane.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ start_sync_task()
|
||||
- let current_depth = 2048 - tx.try_send(Msg::SyncProgress(event.clone()))
|
||||
- .err().map_or(0, |_| 1);
|
||||
- max_queue_depth = max_queue_depth.max(current_depth);
|
||||
- if tx.try_send(Msg::SyncProgress(event.clone())).is_err() {
|
||||
+ // coalesce by lane key at <=30Hz; one send attempt per flush
|
||||
+ coalescer.update(event.clone());
|
||||
+ if let Some(batch) = coalescer.flush_ready() {
|
||||
+ if tx.try_send(Msg::SyncProgressBatch(batch)).is_err() {
|
||||
dropped_count += 1;
|
||||
let _ = tx.try_send(Msg::SyncBackpressureDrop);
|
||||
+ } else {
|
||||
+ max_queue_depth = max_queue_depth.max(observed_queue_depth());
|
||||
+ }
|
||||
}
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If you want, I can produce a single consolidated patch-style rewrite of Sections `4.x`, `5.2/5.4`, `8.2`, `9.3`, and `10.x` so you can drop it directly into iteration 10.
|
||||
177
plans/tui-prd-v2-frankentui.feedback-11.md
Normal file
177
plans/tui-prd-v2-frankentui.feedback-11.md
Normal file
@@ -0,0 +1,177 @@
|
||||
I reviewed the full PRD and avoided everything listed under `## Rejected Recommendations`.
|
||||
These are the highest-impact revisions I’d make.
|
||||
|
||||
1. Stable list pagination via snapshot fences
|
||||
Why this improves the plan: your keyset cursor is deterministic for sort/filter, but still vulnerable to duplicates/skips if sync writes land between page fetches. Add a per-browse snapshot fence so one browse session sees a stable dataset.
|
||||
Tradeoff: newest rows are hidden until refresh, which is correct for deterministic triage.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 5.2 Issue List
|
||||
- **Pagination:** Windowed keyset pagination with explicit cursor state.
|
||||
+ **Pagination:** Windowed keyset pagination with explicit cursor state.
|
||||
+ **Snapshot fence:** On list entry, capture `snapshot_upper_updated_at` (ms) and pin all
|
||||
+ list-page queries to `updated_at <= snapshot_upper_updated_at`. This guarantees no duplicate
|
||||
+ or skipped rows during scrolling even if sync writes occur concurrently.
|
||||
+ A "new data available" badge appears when a newer sync completes; `r` refreshes the fence.
|
||||
|
||||
@@ 5.4 MR List
|
||||
- **Pagination:** Same windowed keyset pagination strategy as Issue List.
|
||||
+ **Pagination:** Same strategy plus snapshot fence (`updated_at <= snapshot_upper_updated_at`)
|
||||
+ for deterministic cross-page traversal under concurrent sync writes.
|
||||
|
||||
@@ 4.7 Navigation Stack Implementation
|
||||
+ Browsing sessions carry a per-screen `BrowseSnapshot` token to preserve stable ordering
|
||||
+ until explicit refresh or screen re-entry.
|
||||
```
|
||||
|
||||
2. Query budgets and soft deadlines
|
||||
Why this improves the plan: currently “slow query” is handled mostly by cancellation and stale-drop. Add explicit latency budgets so UI responsiveness stays predictable under worst-case filters.
|
||||
Tradeoff: sometimes user gets partial/truncated results first, followed by full results on retry/refine.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 4.5 Async Action System
|
||||
+ #### 4.5.2 Query Budgets and Soft Deadlines
|
||||
+ Each query type gets a budget:
|
||||
+ - list window fetch: 120ms target, 250ms hard deadline
|
||||
+ - detail phase-1 metadata: 75ms target, 150ms hard deadline
|
||||
+ - search lexical/hybrid: 250ms hard deadline
|
||||
+ On hard deadline breach, return `QueryDegraded { truncated: true }` and show inline badge:
|
||||
+ "results truncated; refine filter or press r to retry full".
|
||||
+ Implementation uses SQLite progress handler + per-task interrupt deadline.
|
||||
|
||||
@@ 9.3 Phase 0 — Toolchain Gate
|
||||
+ 26. Query deadline behavior validated: hard deadline cancels query and renders degraded badge
|
||||
+ without blocking input loop.
|
||||
```
|
||||
|
||||
3. Targeted cache invalidation and prewarm after sync
|
||||
Why this improves the plan: `invalidate_all()` after sync throws away hot detail cache and hurts the exact post-sync workflow you optimized for. Invalidate only changed keys and prewarm likely-next entities.
|
||||
Tradeoff: slightly more bookkeeping in sync result handling.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 4.1 Module Structure
|
||||
- entity_cache.rs # Bounded LRU cache ... Invalidated on sync completion.
|
||||
+ entity_cache.rs # Bounded LRU cache with selective invalidation by changed EntityKey
|
||||
+ # and optional post-sync prewarm of top changed entities.
|
||||
|
||||
@@ 4.4 App — Implementing the Model Trait (Msg::SyncCompleted)
|
||||
- // Invalidate entity cache — synced data may have changed.
|
||||
- self.entity_cache.invalidate_all();
|
||||
+ // Selective invalidation: evict only changed entities from sync delta.
|
||||
+ self.entity_cache.invalidate_keys(&result.changed_entity_keys);
|
||||
+ // Prewarm top N changed/new entities for immediate post-sync triage.
|
||||
+ self.enqueue_cache_prewarm(&result.changed_entity_keys);
|
||||
```
|
||||
|
||||
4. Exact “what changed” navigation without new DB tables
|
||||
Why this improves the plan: your summary currently uses timestamp filter; this can include unrelated updates and miss edge cases. Keep an in-memory delta ledger per sync run and navigate by exact IDs.
|
||||
Tradeoff: small memory overhead per run; no schema migration required.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 5.9 Sync (Summary mode)
|
||||
-- `i` navigates to Issue List pre-filtered to "since last sync" (using `sync_status.last_completed_at` timestamp comparison)
|
||||
-- `m` navigates to MR List pre-filtered to "since last sync" (using `sync_status.last_completed_at` timestamp comparison)
|
||||
+- `i` navigates to Issue List filtered by exact issue IDs changed in this sync run
|
||||
+- `m` navigates to MR List filtered by exact MR IDs changed in this sync run
|
||||
+ (fallback to timestamp filter only if run delta not available)
|
||||
|
||||
@@ 10.1 New Files
|
||||
+crates/lore-tui/src/sync_delta_ledger.rs # In-memory per-run exact changed/new IDs (issues/MRs/discussions)
|
||||
```
|
||||
|
||||
5. Adaptive render governor (runtime performance safety)
|
||||
Why this improves the plan: capability detection is static; you also need dynamic adaptation when frame time/backpressure worsens (SSH, tmux nesting, huge logs).
|
||||
Tradeoff: visual richness may step down automatically under load.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 3.4.1 Capability-Adaptive Rendering
|
||||
+#### 3.4.2 Adaptive Render Governor
|
||||
+Runtime monitors frame time and stream pressure:
|
||||
+- if frame p95 > 40ms or sync drops spike, switch to lighter profile:
|
||||
+ plain markdown, reduced tree guides, slower spinner tick, less frequent repaint.
|
||||
+- when stable for N seconds, restore previous profile.
|
||||
+CLI override:
|
||||
+`lore tui --render-profile=auto|quality|balanced|speed`
|
||||
|
||||
@@ 9.3 Phase 0 — Toolchain Gate
|
||||
+27. Frame-time governor validated: under induced load, UI remains responsive and input latency
|
||||
+stays within p95 < 75ms while auto-downgrading render profile.
|
||||
```
|
||||
|
||||
6. First-run/data-not-ready screen (not an init wizard)
|
||||
Why this improves the plan: empty DB or missing indexes will otherwise feel broken. A dedicated read-only readiness screen improves first impression and self-recovery.
|
||||
Tradeoff: one extra lightweight screen/state.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 4.3 Core Types (Screen enum)
|
||||
Sync,
|
||||
Stats,
|
||||
Doctor,
|
||||
+ Bootstrap,
|
||||
|
||||
@@ 5.11 Doctor / Stats (Info Screens)
|
||||
+### 5.12 Bootstrap (Data Readiness)
|
||||
+Shown when no synced projects/documents are present or required indexes are missing.
|
||||
+Displays concise readiness checks and exact CLI commands to recover:
|
||||
+`lore sync`, `lore migrate`, `lore --robot doctor`.
|
||||
+Read-only; no auto-execution.
|
||||
```
|
||||
|
||||
7. Global project scope pinning across screens
|
||||
Why this improves the plan: users repeatedly apply the same project filter across dashboard/list/search/timeline/who. Add a global scope pin to reduce repetitive filtering and speed triage.
|
||||
Tradeoff: must show clear “scope active” indicator to avoid confusion.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 4.1 Module Structure
|
||||
+ scope.rs # Global project scope context (all-projects or pinned project set)
|
||||
|
||||
@@ 8.1 Global (Available Everywhere)
|
||||
+| `P` | Open project scope picker / toggle global scope pin |
|
||||
|
||||
@@ 4.10 State Module — Complete
|
||||
+pub global_scope: ScopeContext, // Applies to dashboard/list/search/timeline/who queries
|
||||
|
||||
@@ 10.11 Action Module — Query Bridge
|
||||
- pub fn fetch_issues(conn: &Connection, filter: &IssueFilter) -> Result<Vec<IssueListRow>, LoreError>
|
||||
+ pub fn fetch_issues(conn: &Connection, scope: &ScopeContext, filter: &IssueFilter) -> Result<Vec<IssueListRow>, LoreError>
|
||||
```
|
||||
|
||||
8. Concurrency correctness tests for pagination and cancellation races
|
||||
Why this improves the plan: current reliability tests are good, but missing a direct test for duplicate/skip behavior under concurrent sync writes while paginating.
|
||||
Tradeoff: additional integration test complexity.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 9.2 Phases (Phase 5.5 — Reliability Test Pack)
|
||||
+ Concurrent pagination/write race tests :p55j, after p55h, 1d
|
||||
+ Query deadline cancellation race tests :p55k, after p55j, 0.5d
|
||||
|
||||
@@ 9.3 Phase 0 — Toolchain Gate
|
||||
+28. Concurrent pagination/write test proves no duplicates/skips within a pinned browse snapshot.
|
||||
+29. Cancellation race test proves no cross-task interrupt bleed and no stuck loading state.
|
||||
```
|
||||
|
||||
9. URL opening policy v2: allowlisted GitLab entity paths
|
||||
Why this improves the plan: host validation is necessary but not always sufficient. Restrict default browser opens to known GitLab entity paths and require confirmation for unusual paths on same host.
|
||||
Tradeoff: occasional extra prompt for uncommon but valid URLs.
|
||||
|
||||
```diff
|
||||
diff --git a/docs/plans/gitlore-tui-prd-v2.md b/docs/plans/gitlore-tui-prd-v2.md
|
||||
@@ 3.1 Risk Matrix
|
||||
-| Malicious URL in entity data opened in browser | Medium | Low | URL host validated against configured GitLab instance before `open`/`xdg-open` |
|
||||
+| Malicious URL in entity data opened in browser | Medium | Low | Validate scheme+host+port and path pattern allowlist (`/-/issues/`, `/-/merge_requests/`, project issue/MR routes). Unknown same-host paths require explicit confirm modal. |
|
||||
|
||||
@@ 10.4.1 Terminal Safety — Untrusted Text Sanitization
|
||||
- pub fn is_safe_url(url: &str, allowed_origins: &[AllowedOrigin]) -> bool
|
||||
+ pub fn classify_safe_url(url: &str, policy: &UrlPolicy) -> UrlSafety
|
||||
+ // UrlSafety::{AllowedEntityPath, AllowedButUnrecognizedPath, Blocked}
|
||||
```
|
||||
|
||||
These 9 changes are additive, avoid previously rejected ideas, and materially improve determinism, responsiveness, post-sync usefulness, and safety without forcing a big architecture reset.
|
||||
203
plans/tui-prd-v2-frankentui.feedback-2.md
Normal file
203
plans/tui-prd-v2-frankentui.feedback-2.md
Normal file
@@ -0,0 +1,203 @@
|
||||
I excluded the two items in your `## Rejected Recommendations` and focused on net-new improvements.
|
||||
These are the highest-impact revisions I’d make.
|
||||
|
||||
### 1. Fix the package graph now (avoid a hard Cargo cycle)
|
||||
Your current plan has `root -> optional lore-tui` and `lore-tui -> lore (root)`, which creates a cyclic dependency risk. Split shared logic into a dedicated core crate so CLI and TUI both depend downward.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 9.1 Dependency Changes
|
||||
-[workspace]
|
||||
-members = [".", "crates/lore-tui"]
|
||||
+[workspace]
|
||||
+members = [".", "crates/lore-core", "crates/lore-tui"]
|
||||
|
||||
@@
|
||||
-[dependencies]
|
||||
-lore-tui = { path = "crates/lore-tui", optional = true }
|
||||
+[dependencies]
|
||||
+lore-core = { path = "crates/lore-core" }
|
||||
+lore-tui = { path = "crates/lore-tui", optional = true }
|
||||
|
||||
@@ # crates/lore-tui/Cargo.toml
|
||||
-lore = { path = "../.." } # Core lore library
|
||||
+lore-core = { path = "../lore-core" } # Shared domain/query crate (acyclic graph)
|
||||
```
|
||||
|
||||
### 2. Stop coupling TUI to `cli/commands/*` internals
|
||||
Calling CLI command modules from TUI is brittle and will drift. Introduce a shared query/service layer with DTOs owned by core.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 4.1 Module Structure
|
||||
- action.rs # Async action runners (DB queries, GitLab calls)
|
||||
+ action.rs # Task dispatch only
|
||||
+ service/
|
||||
+ mod.rs
|
||||
+ query.rs # Shared read services (CLI + TUI)
|
||||
+ sync.rs # Shared sync orchestration facade
|
||||
+ dto.rs # UI-agnostic data contracts
|
||||
|
||||
@@ ## 10.2 Modified Files
|
||||
-src/cli/commands/list.rs # Extract query_issues(), query_mrs() as pub fns
|
||||
-src/cli/commands/show.rs # Extract query_issue_detail(), query_mr_detail() as pub fns
|
||||
-src/cli/commands/who.rs # Extract query_experts(), etc. as pub fns
|
||||
-src/cli/commands/search.rs # Extract run_search_query() as pub fn
|
||||
+crates/lore-core/src/query/issues.rs # Canonical issue queries
|
||||
+crates/lore-core/src/query/mrs.rs # Canonical MR queries
|
||||
+crates/lore-core/src/query/show.rs # Canonical detail queries
|
||||
+crates/lore-core/src/query/who.rs # Canonical people queries
|
||||
+crates/lore-core/src/query/search.rs # Canonical search queries
|
||||
+src/cli/commands/*.rs # Consume lore-core query services
|
||||
+crates/lore-tui/src/action.rs # Consume lore-core query services
|
||||
```
|
||||
|
||||
### 3. Add a real task supervisor (dedupe + cancellation + priority)
|
||||
Right now tasks are ad hoc and can overrun each other. Add a scheduler keyed by screen+intent.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 4.5 Async Action System
|
||||
-The `Cmd::task(|| { ... })` pattern runs a blocking closure on a background thread pool.
|
||||
+The TUI uses a `TaskSupervisor`:
|
||||
+- Keyed tasks (`TaskKey`) to dedupe redundant requests
|
||||
+- Priority lanes (`Input`, `Navigation`, `Background`)
|
||||
+- Cooperative cancellation tokens per task
|
||||
+- Late-result drop via generation IDs (not just search)
|
||||
|
||||
@@ ## 4.3 Core Types
|
||||
+pub enum TaskKey {
|
||||
+ LoadScreen(Screen),
|
||||
+ Search { generation: u64 },
|
||||
+ SyncStream,
|
||||
+}
|
||||
```
|
||||
|
||||
### 4. Correct sync streaming architecture (current sketch loses streamed events)
|
||||
The sample creates `tx/rx` then drops `rx`; events never reach update loop. Define an explicit stream subscription with bounded queue and backpressure policy.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 4.4 App — Implementing the Model Trait
|
||||
- let (tx, _rx) = std::sync::mpsc::channel::<Msg>();
|
||||
+ let (tx, rx) = std::sync::mpsc::sync_channel::<Msg>(1024);
|
||||
+ // rx is registered via Subscription::from_receiver("sync-stream", rx)
|
||||
|
||||
@@
|
||||
- let result = crate::ingestion::orchestrator::run_sync(
|
||||
+ let result = crate::ingestion::orchestrator::run_sync(
|
||||
&config,
|
||||
&conn,
|
||||
|event| {
|
||||
@@
|
||||
- let _ = tx.send(Msg::SyncProgress(event.clone()));
|
||||
- let _ = tx.send(Msg::SyncLogLine(format!("{event:?}")));
|
||||
+ if tx.try_send(Msg::SyncProgress(event.clone())).is_err() {
|
||||
+ let _ = tx.try_send(Msg::SyncBackpressureDrop);
|
||||
+ }
|
||||
+ let _ = tx.try_send(Msg::SyncLogLine(format!("{event:?}")));
|
||||
},
|
||||
);
|
||||
```
|
||||
|
||||
### 5. Upgrade data-plane performance plan (keyset pagination + index contracts)
|
||||
Virtualized list without keyset paging still forces expensive scans. Add explicit keyset pagination and query-plan CI checks.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 9.3 Phase 0 — Toolchain Gate
|
||||
-7. p95 list query latency < 75ms on synthetic fixture (10k issues, 5k MRs)
|
||||
+7. p95 list page fetch latency < 75ms using keyset pagination (10k issues, 5k MRs)
|
||||
+8. EXPLAIN QUERY PLAN must show index usage for top 10 TUI queries
|
||||
+9. No full table scan on issues/MRs/discussions under default filters
|
||||
|
||||
@@
|
||||
-8. p95 search latency < 200ms on synthetic fixture (50k documents, lexical mode)
|
||||
+10. p95 search latency < 200ms on synthetic fixture (50k documents, lexical mode)
|
||||
|
||||
+## 9.4 Required Indexes (GA blocker)
|
||||
+- `issues(project_id, state, updated_at DESC, iid DESC)`
|
||||
+- `merge_requests(project_id, state, updated_at DESC, iid DESC)`
|
||||
+- `discussions(project_id, entity_type, entity_iid, created_at DESC)`
|
||||
+- `notes(discussion_id, created_at ASC)`
|
||||
```
|
||||
|
||||
### 6. Enforce `EntityKey` everywhere (remove bare IID paths)
|
||||
You correctly identified multi-project IID collisions, but many message/state signatures still use `i64`. Make `EntityKey` mandatory in all navigation and detail loaders.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 4.3 Core Types
|
||||
- IssueSelected(i64),
|
||||
+ IssueSelected(EntityKey),
|
||||
@@
|
||||
- MrSelected(i64),
|
||||
+ MrSelected(EntityKey),
|
||||
@@
|
||||
- IssueDetailLoaded(IssueDetail),
|
||||
+ IssueDetailLoaded { key: EntityKey, detail: IssueDetail },
|
||||
@@
|
||||
- MrDetailLoaded(MrDetail),
|
||||
+ MrDetailLoaded { key: EntityKey, detail: MrDetail },
|
||||
|
||||
@@ ## 10.10 State Module — Complete
|
||||
- Cmd::msg(Msg::NavigateTo(Screen::IssueDetail(iid)))
|
||||
+ Cmd::msg(Msg::NavigateTo(Screen::IssueDetail(entity_key)))
|
||||
```
|
||||
|
||||
### 7. Harden filter/search semantics (strict parser + inline diagnostics + explain scores)
|
||||
Current filter parser silently ignores unknown fields; that causes hidden mistakes. Add strict parse diagnostics and search score explainability.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 10.12.1 Filter Bar Widget
|
||||
- _ => {} // Unknown fields silently ignored
|
||||
+ _ => self.errors.push(format!("Unknown filter field: {}", token.field))
|
||||
|
||||
+ pub errors: Vec<String>, // inline parse/validation errors
|
||||
+ pub warnings: Vec<String>, // non-fatal coercions
|
||||
|
||||
@@ ## 5.6 Search
|
||||
-- **Live preview:** Selected result shows snippet + metadata in right pane
|
||||
+- **Live preview:** Selected result shows snippet + metadata in right pane
|
||||
+- **Explain score:** Optional breakdown (lexical, semantic, recency, boosts) for trust/debug
|
||||
```
|
||||
|
||||
### 8. Add operational resilience: safe mode + panic report + startup fallback
|
||||
TUI failures should degrade gracefully, not block usage.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 3.1 Risk Matrix
|
||||
+| Runtime panic leaves user blocked | High | Medium | Panic hook writes crash report, restores terminal, offers fallback CLI command |
|
||||
|
||||
@@ ## 10.3 Entry Point
|
||||
+pub fn launch_tui(config: Config, db_path: &Path) -> Result<(), LoreError> {
|
||||
+ install_panic_hook_for_tui(); // terminal restore + crash dump path
|
||||
+ ...
|
||||
+}
|
||||
|
||||
@@ ## 8.1 Global (Available Everywhere)
|
||||
+| `:` | Show fallback equivalent CLI command for current screen/action |
|
||||
```
|
||||
|
||||
### 9. Add a “jump list” (forward/back navigation, not only stack pop)
|
||||
Current model has only push/pop and reset. Add browser-like history for investigation workflows.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ ## 4.7 Navigation Stack Implementation
|
||||
pub struct NavigationStack {
|
||||
- stack: Vec<Screen>,
|
||||
+ back_stack: Vec<Screen>,
|
||||
+ current: Screen,
|
||||
+ forward_stack: Vec<Screen>,
|
||||
+ jump_list: Vec<Screen>, // recent entity/detail hops
|
||||
}
|
||||
|
||||
@@ ## 8.1 Global (Available Everywhere)
|
||||
+| `Ctrl+o` | Jump backward in jump list |
|
||||
+| `Ctrl+i` | Jump forward in jump list |
|
||||
```
|
||||
|
||||
If you want, I can produce a single consolidated “PRD v2.1” patch that applies all nine revisions coherently section-by-section.
|
||||
163
plans/tui-prd-v2-frankentui.feedback-3.md
Normal file
163
plans/tui-prd-v2-frankentui.feedback-3.md
Normal file
@@ -0,0 +1,163 @@
|
||||
I excluded everything already listed in `## Rejected Recommendations`.
|
||||
These are the highest-impact net-new revisions I’d make.
|
||||
|
||||
1. **Enforce Entity Identity Consistency End-to-End (P0)**
|
||||
Analysis: The PRD defines `EntityKey`, but many code paths still pass bare `iid` (`IssueSelected(item.iid)`, timeline refs, search refs). In multi-project datasets this will cause wrong-entity navigation and subtle data corruption in cached state. Make `EntityKey` mandatory in every navigation message and add compile-time constructors.
|
||||
```diff
|
||||
@@ 4.3 Core Types
|
||||
pub struct EntityKey {
|
||||
pub project_id: i64,
|
||||
pub iid: i64,
|
||||
pub kind: EntityKind,
|
||||
}
|
||||
+impl EntityKey {
|
||||
+ pub fn issue(project_id: i64, iid: i64) -> Self { Self { project_id, iid, kind: EntityKind::Issue } }
|
||||
+ pub fn mr(project_id: i64, iid: i64) -> Self { Self { project_id, iid, kind: EntityKind::MergeRequest } }
|
||||
+}
|
||||
|
||||
@@ 10.10 state/issue_list.rs
|
||||
- .map(|item| Msg::IssueSelected(item.iid))
|
||||
+ .map(|item| Msg::IssueSelected(EntityKey::issue(item.project_id, item.iid)))
|
||||
|
||||
@@ 10.10 state/mr_list.rs
|
||||
- .map(|item| Msg::MrSelected(item.iid))
|
||||
+ .map(|item| Msg::MrSelected(EntityKey::mr(item.project_id, item.iid)))
|
||||
```
|
||||
|
||||
2. **Make TaskSupervisor Mandatory for All Background Work (P0)**
|
||||
Analysis: The plan introduces `TaskSupervisor` but still dispatches many direct `Cmd::task` calls. That will reintroduce stale updates, duplicate queries, and priority inversion under rapid input. Centralize all background task creation through one spawn path that enforces dedupe, cancellation tokening, and generation checks.
|
||||
```diff
|
||||
@@ 4.5.1 Task Supervisor (Dedup + Cancellation + Priority)
|
||||
-The supervisor is owned by `LoreApp` and consulted before dispatching any `Cmd::task`.
|
||||
+The supervisor is owned by `LoreApp` and is the ONLY allowed path for background work.
|
||||
+All task launches use `LoreApp::spawn_task(TaskKey, TaskPriority, closure)`.
|
||||
|
||||
@@ 4.4 App — Implementing the Model Trait
|
||||
- Cmd::task(move || { ... })
|
||||
+ self.spawn_task(TaskKey::LoadScreen(screen.clone()), TaskPriority::Navigation, move |token| { ... })
|
||||
```
|
||||
|
||||
3. **Remove the Sync Streaming TODO and Make Real-Time Streaming a GA Gate (P0)**
|
||||
Analysis: Current text admits sync progress is buffered with a TODO. That undercuts one of the main value props. Make streaming progress/log delivery non-optional, with bounded buffers and dropped-line accounting.
|
||||
```diff
|
||||
@@ 4.4 start_sync_task()
|
||||
- // TODO: Register rx as subscription when FrankenTUI supports it.
|
||||
- // For now, the task returns the final Msg and progress is buffered.
|
||||
+ // Register rx as a live subscription (`Subscription::from_receiver` adapter).
|
||||
+ // Progress and logs must render in real time (no batch-at-end fallback).
|
||||
+ // Keep a bounded ring buffer (N=5000) and surface `dropped_log_lines` in UI.
|
||||
|
||||
@@ 9.3 Phase 0 — Toolchain Gate
|
||||
+11. Real-time sync stream verified: progress updates visible during run, not only at completion.
|
||||
```
|
||||
|
||||
4. **Upgrade List/Search Data Strategy to Windowed Keyset + Prefetch (P0)**
|
||||
Analysis: “Virtualized list” alone does not solve query/transfer cost if full result sets are loaded. Move to fixed-size keyset windows with next-window prefetch and fast first paint; this keeps latency predictable on 100k+ records.
|
||||
```diff
|
||||
@@ 5.2 Issue List
|
||||
- Pagination: Virtual scrolling for large result sets
|
||||
+ Pagination: Windowed keyset pagination (window=200 rows) with background prefetch of next window.
|
||||
+ First paint uses current window only; no full-result materialization.
|
||||
|
||||
@@ 5.4 MR List
|
||||
+ Same windowed keyset pagination strategy as Issue List.
|
||||
|
||||
@@ 9.3 Success criteria
|
||||
- 7. p95 list page fetch latency < 75ms using keyset pagination on synthetic fixture (10k issues, 5k MRs)
|
||||
+ 7. p95 first-paint latency < 50ms and p95 next-window fetch < 75ms on synthetic fixture (100k issues, 50k MRs)
|
||||
```
|
||||
|
||||
5. **Add Resumable Sync Checkpoints + Per-Project Fault Isolation (P1)**
|
||||
Analysis: If sync is interrupted or one project fails, current design mostly falls back to cancel/fail. Add checkpoints so long runs can resume, and isolate failures to project/resource scope while continuing others.
|
||||
```diff
|
||||
@@ 3.1 Risk Matrix
|
||||
+| Interrupted sync loses progress | High | Medium | Persist phase checkpoints and offer resume |
|
||||
|
||||
@@ 5.9 Sync
|
||||
+Running mode: failed project/resource lanes are marked degraded while other lanes continue.
|
||||
+Summary mode: offer `[R]esume interrupted sync` from last checkpoint.
|
||||
|
||||
@@ 11 Assumptions
|
||||
-16. No new SQLite tables needed (but required indexes must be verified — see Performance SLOs).
|
||||
+16. Add minimal internal tables for reliability: `sync_runs` and `sync_checkpoints` (append-only metadata).
|
||||
```
|
||||
|
||||
6. **Add Capability-Adaptive Rendering Modes (P1)**
|
||||
Analysis: Terminal compatibility is currently test-focused, but runtime adaptation is under-specified. Add explicit degradations for no-truecolor, no-unicode, slow SSH/tmux paths to reduce rendering artifacts and support incidents.
|
||||
```diff
|
||||
@@ 3.4 Terminal Compatibility Testing
|
||||
+Add capability matrix validation: truecolor/256/16 color, unicode/ascii glyphs, alt-screen on/off.
|
||||
|
||||
@@ 10.19 CLI Integration
|
||||
+Tui {
|
||||
+ #[arg(long, default_value="auto")] render_mode: String, // auto|full|minimal
|
||||
+ #[arg(long)] ascii: bool,
|
||||
+ #[arg(long)] no_alt_screen: bool,
|
||||
+}
|
||||
```
|
||||
|
||||
7. **Harden Browser/Open and Log Privacy (P1)**
|
||||
Analysis: `open_current_in_browser` currently trusts stored URLs; sync logs may expose tokens/emails from upstream messages. Add host allowlisting and redaction pipeline by default.
|
||||
```diff
|
||||
@@ 4.4 open_current_in_browser()
|
||||
- if let Some(url) = url { ... open ... }
|
||||
+ if let Some(url) = url {
|
||||
+ if !self.state.security.is_allowed_gitlab_url(&url) {
|
||||
+ self.state.set_error("Blocked non-GitLab URL".into());
|
||||
+ return;
|
||||
+ }
|
||||
+ ... open ...
|
||||
+ }
|
||||
|
||||
@@ 5.9 Sync
|
||||
+Log stream passes through redaction (tokens, auth headers, email local-parts) before render/storage.
|
||||
```
|
||||
|
||||
8. **Add “My Workbench” Screen for Daily Pull (P1, new feature)**
|
||||
Analysis: The PRD is strong on exploration, weaker on “what should I do now?”. Add a focused operator screen aggregating assigned issues, requested reviews, unresolved threads mentioning me, and stale approvals. This makes the TUI habit-forming.
|
||||
```diff
|
||||
@@ 5. Screen Taxonomy
|
||||
+### 5.12 My Workbench
|
||||
+Single-screen triage cockpit:
|
||||
+- Assigned-to-me open issues/MRs
|
||||
+- Review requests awaiting action
|
||||
+- Threads mentioning me and unresolved
|
||||
+- Recently stale approvals / blocked MRs
|
||||
|
||||
@@ 8.1 Global
|
||||
+| `gb` | Go to My Workbench |
|
||||
|
||||
@@ 9.2 Phases
|
||||
+section Phase 3.5 — Daily Workflow
|
||||
+My Workbench screen + queries :p35a, after p3d, 2d
|
||||
```
|
||||
|
||||
9. **Add Rollout, SLO Telemetry, and Kill-Switch Plan (P0)**
|
||||
Analysis: You have implementation phases but no production rollout control. Add explicit experiment flags, health telemetry, and rollback criteria so risk is operationally bounded.
|
||||
```diff
|
||||
@@ Table of Contents
|
||||
-11. [Assumptions](#11-assumptions)
|
||||
+11. [Assumptions](#11-assumptions)
|
||||
+12. [Rollout & Telemetry](#12-rollout--telemetry)
|
||||
|
||||
@@ NEW SECTION 12
|
||||
+## 12. Rollout & Telemetry
|
||||
+- Feature flags: `tui_experimental`, `tui_sync_streaming`, `tui_workbench`
|
||||
+- Metrics: startup_ms, frame_render_p95_ms, db_busy_rate, panic_free_sessions, sync_drop_events
|
||||
+- Kill-switch: disable `tui` feature path at runtime if panic rate > 0.5% sessions over 24h
|
||||
+- Canary rollout: internal only -> opt-in beta -> default-on
|
||||
```
|
||||
|
||||
10. **Strengthen Reliability Pack with Event-Fuzz + Soak Tests (P0)**
|
||||
Analysis: Current tests are good but still light on prolonged event pressure. Add deterministic fuzzed key/resize/paste streams and a long soak to catch rare deadlocks/leaks and state corruption.
|
||||
```diff
|
||||
@@ 9.2 Phase 5.5 — Reliability Test Pack
|
||||
+Event fuzz tests (key/resize/paste interleavings) :p55g, after p55e, 1d
|
||||
+30-minute soak test (no panic, bounded memory) :p55h, after p55g, 1d
|
||||
|
||||
@@ 9.3 Success criteria
|
||||
+12. Event-fuzz suite passes with zero invariant violations across 10k randomized traces.
|
||||
+13. 30-minute soak: no panic, no deadlock, memory growth < 5%.
|
||||
```
|
||||
|
||||
If you want, I can produce a single consolidated unified diff of the full PRD text next (all edits merged, ready to apply as v3).
|
||||
157
plans/tui-prd-v2-frankentui.feedback-4.md
Normal file
157
plans/tui-prd-v2-frankentui.feedback-4.md
Normal file
@@ -0,0 +1,157 @@
|
||||
Below are my strongest revisions, focused on correctness, reliability, and long-term maintainability, while avoiding all items in your `## Rejected Recommendations`.
|
||||
|
||||
1. **Fix the Cargo/toolchain architecture (current plan has a real dependency-cycle risk and shaky per-member toolchain behavior).**
|
||||
Analysis: The current plan has `lore -> lore-tui (optional)` and `lore-tui -> lore`, which creates a package cycle when `tui` is enabled. Also, per-member `rust-toolchain.toml` in a workspace is easy to misapply in CI/dev workflows. The cleanest robust shape is: `lore-tui` is a separate binary crate (nightly), `lore` remains stable and delegates at runtime (`lore tui` shells out to `lore-tui`).
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 3.2 Nightly Rust Strategy
|
||||
-- The `lore` binary integrates TUI via `lore tui` subcommand. The `lore-tui` crate is a library dependency feature-gated in the root.
|
||||
+- `lore-tui` is a separate binary crate built on pinned nightly.
|
||||
+- `lore` (stable) does not compile-link `lore-tui`; `lore tui` delegates by spawning `lore-tui`.
|
||||
+- This removes Cargo dependency-cycle risk and keeps stable builds nightly-free.
|
||||
@@ 9.1 Dependency Changes
|
||||
-[features]
|
||||
-tui = ["dep:lore-tui"]
|
||||
-[dependencies]
|
||||
-lore-tui = { path = "crates/lore-tui", optional = true }
|
||||
+[dependencies]
|
||||
+# no compile-time dependency on lore-tui from lore
|
||||
+# runtime delegation keeps toolchains isolated
|
||||
@@ 10.19 CLI Integration
|
||||
-Add Tui match arm that directly calls crate::tui::launch_tui(...)
|
||||
+Add Tui match arm that resolves and spawns `lore-tui` with passthrough args.
|
||||
+If missing, print actionable install/build command.
|
||||
```
|
||||
|
||||
2. **Make `TaskSupervisor` the *actual* single async path (remove contradictory direct `Cmd::task` usage in state handlers).**
|
||||
Analysis: You declare “direct `Cmd::task` is prohibited outside supervisor,” but later `handle_screen_msg` still launches tasks directly. That contradiction will reintroduce stale-result bugs and race conditions. Make state handlers pure (intent-only); all async launch/cancel/dedup goes through one supervised API.
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 4.5.1 Task Supervisor
|
||||
-The supervisor is the ONLY allowed path for background work.
|
||||
+The supervisor is the ONLY allowed path for background work, enforced by architecture:
|
||||
+`AppState` emits intents only; `LoreApp::update` launches tasks via `spawn_task(...)`.
|
||||
@@ 10.10 State Module — Complete
|
||||
-pub fn handle_screen_msg(..., db: &Arc<Mutex<Connection>>) -> Cmd<Msg>
|
||||
+pub fn handle_screen_msg(...) -> ScreenIntent
|
||||
+// no DB access, no Cmd::task in state layer
|
||||
```
|
||||
|
||||
3. **Enforce `EntityKey` everywhere (remove raw IID navigation paths).**
|
||||
Analysis: Multi-project identity is one of your strongest ideas, but multiple snippets still navigate by bare IID (`document_id`, `EntityRef::Issue(i64)`). That can misroute across projects and create silent correctness bugs. Make all navigation-bearing results carry `EntityKey` end-to-end.
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 4.3 Core Types
|
||||
-pub enum EntityRef { Issue(i64), MergeRequest(i64) }
|
||||
+pub enum EntityRef { Issue(EntityKey), MergeRequest(EntityKey) }
|
||||
@@ 10.10 state/search.rs
|
||||
-Some(Msg::NavigateTo(Screen::IssueDetail(r.document_id)))
|
||||
+Some(Msg::NavigateTo(Screen::IssueDetail(r.entity_key.clone())))
|
||||
@@ 10.11 action.rs
|
||||
-pub fn fetch_issue_detail(conn: &Connection, iid: i64) -> Result<IssueDetail, LoreError>
|
||||
+pub fn fetch_issue_detail(conn: &Connection, key: &EntityKey) -> Result<IssueDetail, LoreError>
|
||||
```
|
||||
|
||||
4. **Introduce a shared query boundary inside the existing crate (not a new crate) to decouple TUI from CLI presentation structs.**
|
||||
Analysis: Reusing CLI command modules directly is fast initially, but it ties TUI to output-layer types and command concerns. A minimal internal `core::query::*` module gives a stable data contract used by both CLI and TUI without the overhead of a new crate split.
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 10.2 Modified Files
|
||||
-src/cli/commands/list.rs # extract query_issues/query_mrs as pub
|
||||
-src/cli/commands/show.rs # extract query_issue_detail/query_mr_detail as pub
|
||||
+src/core/query/mod.rs
|
||||
+src/core/query/issues.rs
|
||||
+src/core/query/mrs.rs
|
||||
+src/core/query/detail.rs
|
||||
+src/core/query/search.rs
|
||||
+src/core/query/who.rs
|
||||
+src/cli/commands/* now call core::query::* + format output
|
||||
+TUI action.rs calls core::query::* directly
|
||||
```
|
||||
|
||||
5. **Add terminal-safety sanitization for untrusted text (ANSI/OSC injection hardening).**
|
||||
Analysis: Issue/MR bodies, notes, and logs are untrusted text in a terminal context. Without sanitization, terminal escape/control sequences can spoof UI or trigger unintended behavior. Add explicit sanitization and a strict URL policy before rendering/opening.
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 3.1 Risk Matrix
|
||||
+| Terminal escape/control-sequence injection via issue/note text | High | Medium | Strip ANSI/OSC/control chars before render; escape markdown output; allowlist URL scheme+host |
|
||||
@@ 4.1 Module Structure
|
||||
+ safety.rs # sanitize_for_terminal(), safe_url_policy()
|
||||
@@ 10.5/10.8/10.14/10.16
|
||||
+All user-sourced text passes through `sanitize_for_terminal()` before widget rendering.
|
||||
+Disable markdown raw HTML and clickable links unless URL policy passes.
|
||||
```
|
||||
|
||||
6. **Move resumable sync checkpoints into v1 (lightweight version).**
|
||||
Analysis: You already identify interruption risk as real. Deferring resumability to post-v1 leaves a major reliability gap in exactly the heaviest workflow. A lightweight checkpoint table (resource cursor + updated-at watermark) gives large reliability gain with modest complexity.
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 3.1 Risk Matrix
|
||||
-- Resumable checkpoints planned for post-v1
|
||||
+Resumable checkpoints included in v1 (lightweight cursors per project/resource lane)
|
||||
@@ 9.3 Success Criteria
|
||||
+14. Interrupt-and-resume test: sync resumes from checkpoint and reaches completion without full restart.
|
||||
@@ 9.3.1 Required Indexes (GA Blocker)
|
||||
+CREATE TABLE IF NOT EXISTS sync_checkpoints (
|
||||
+ project_id INTEGER NOT NULL,
|
||||
+ lane TEXT NOT NULL,
|
||||
+ cursor TEXT,
|
||||
+ updated_at_ms INTEGER NOT NULL,
|
||||
+ PRIMARY KEY (project_id, lane)
|
||||
+);
|
||||
```
|
||||
|
||||
7. **Strengthen performance gates with tiered fixtures and memory ceilings.**
|
||||
Analysis: Current thresholds are good, but fixture sizes are too close to mid-scale only. Add S/M/L fixtures and memory budget checks so regressions appear before real-world datasets hit them. This gives much more confidence in long-term scalability.
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 9.3 Phase 0 — Toolchain Gate
|
||||
-7. p95 first-paint latency < 50ms ... (100k issues, 50k MRs)
|
||||
-10. p95 search latency < 200ms ... (50k documents)
|
||||
+7. Tiered fixtures:
|
||||
+ S: 10k issues / 5k MRs / 50k notes
|
||||
+ M: 100k issues / 50k MRs / 500k notes
|
||||
+ L: 250k issues / 100k MRs / 1M notes
|
||||
+ Enforce p95 targets per tier and memory ceiling (<250MB RSS in M tier).
|
||||
+10. Search SLO validated in S and M tiers, lexical and hybrid modes.
|
||||
```
|
||||
|
||||
8. **Add session restore (last screen + filters + selection), with explicit `--fresh` opt-out.**
|
||||
Analysis: This is high-value daily UX with low complexity, and it makes the TUI feel materially more “compelling/useful” without feature bloat. It also reduces friction when recovering from crash/restart.
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 1. Executive Summary
|
||||
+- **Session restore** — resume last screen, filters, and selection on startup.
|
||||
@@ 4.1 Module Structure
|
||||
+ session.rs # persisted UI session state
|
||||
@@ 8.1 Global
|
||||
+| `Ctrl+R` | Reset session state for current screen |
|
||||
@@ 10.19 CLI Integration
|
||||
+`lore tui --fresh` starts without restoring prior session state.
|
||||
@@ 11. Assumptions
|
||||
-12. No TUI-specific configuration initially.
|
||||
+12. Minimal TUI state file is allowed for session restore only.
|
||||
```
|
||||
|
||||
9. **Add parity tests between TUI data panels and `--robot` outputs.**
|
||||
Analysis: You already have `ShowCliEquivalent`; parity tests make that claim trustworthy and prevent drift between interfaces. This is a strong reliability multiplier and helps future refactors.
|
||||
```diff
|
||||
--- a/Gitlore_TUI_PRD_v2.md
|
||||
+++ b/Gitlore_TUI_PRD_v2.md
|
||||
@@ 9.2 Phases / 9.3 Success Criteria
|
||||
+Phase 5.6 — CLI/TUI Parity Pack
|
||||
+ - Dashboard count parity vs `lore --robot count/status`
|
||||
+ - List/detail parity for issues/MRs on sampled entities
|
||||
+ - Search result identity parity (top-N ids) for lexical mode
|
||||
+Success criterion: parity suite passes on CI fixtures.
|
||||
```
|
||||
|
||||
If you want, I can produce a single consolidated patch of the PRD text (one unified diff) so you can drop it directly into the next iteration.
|
||||
200
plans/tui-prd-v2-frankentui.feedback-5.md
Normal file
200
plans/tui-prd-v2-frankentui.feedback-5.md
Normal file
@@ -0,0 +1,200 @@
|
||||
1. **Fix the structural inconsistency between `src/tui` and `crates/lore-tui/src`**
|
||||
Analysis: The PRD currently defines two different code layouts for the same system. That will cause implementation drift, wrong imports, and duplicated modules. Locking to one canonical layout early prevents execution churn and makes every snippet/action item unambiguous.
|
||||
|
||||
```diff
|
||||
@@ 4.1 Module Structure @@
|
||||
-src/
|
||||
- tui/
|
||||
+crates/lore-tui/src/
|
||||
mod.rs
|
||||
app.rs
|
||||
message.rs
|
||||
@@
|
||||
-### 10.5 Dashboard View (FrankenTUI Native)
|
||||
-// src/tui/view/dashboard.rs
|
||||
+### 10.5 Dashboard View (FrankenTUI Native)
|
||||
+// crates/lore-tui/src/view/dashboard.rs
|
||||
@@
|
||||
-### 10.6 Sync View
|
||||
-// src/tui/view/sync.rs
|
||||
+### 10.6 Sync View
|
||||
+// crates/lore-tui/src/view/sync.rs
|
||||
```
|
||||
|
||||
2. **Add a small `ui_adapter` seam to contain FrankenTUI API churn**
|
||||
Analysis: You already identified high likelihood of upstream breakage. Pinning a commit helps, but if every screen imports raw `ftui_*` types directly, churn ripples through dozens of files. A thin adapter layer reduces upgrade cost without introducing the rejected “full portability abstraction”.
|
||||
|
||||
```diff
|
||||
@@ 3.1 Risk Matrix @@
|
||||
| API breaking changes | High | High (v0.x) | Pin exact git commit; vendor source if needed |
|
||||
+| API breakage blast radius across app code | High | High | Constrain ftui usage behind `ui_adapter/*` wrappers |
|
||||
|
||||
@@ 4.1 Module Structure @@
|
||||
+ ui_adapter/
|
||||
+ mod.rs # Re-export stable local UI primitives
|
||||
+ runtime.rs # App launch/options wrappers
|
||||
+ widgets.rs # Table/List/Modal wrapper constructors
|
||||
+ input.rs # Text input + focus helpers
|
||||
|
||||
@@ 9.3 Phase 0 — Toolchain Gate @@
|
||||
+14. `ui_adapter` compile-check: no screen module imports `ftui_*` directly (lint-enforced)
|
||||
```
|
||||
|
||||
3. **Correct search mode behavior and replace sleep-based debounce with cancelable scheduling**
|
||||
Analysis: Current plan hardcodes `"hybrid"` in `execute_search`, so mode switching is UI-only and incorrect. Also, spawning sleeping tasks per keypress is wasteful under fast typing. Make mode a first-class query parameter and debounce via one cancelable scheduled event per input domain.
|
||||
|
||||
```diff
|
||||
@@ 4.4 maybe_debounced_query @@
|
||||
-std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
-match crate::tui::action::execute_search(&conn, &query, &filters) {
|
||||
+// no thread sleep; schedule SearchRequestStarted after 200ms via debounce scheduler
|
||||
+match crate::tui::action::execute_search(&conn, &query, &filters, mode) {
|
||||
|
||||
@@ 10.11 Action Module — Query Bridge @@
|
||||
-pub fn execute_search(conn: &Connection, query: &str, filters: &SearchCliFilters) -> Result<SearchResponse, LoreError> {
|
||||
- let mode_str = "hybrid"; // default; TUI mode selector overrides
|
||||
+pub fn execute_search(
|
||||
+ conn: &Connection,
|
||||
+ query: &str,
|
||||
+ filters: &SearchCliFilters,
|
||||
+ mode: SearchMode,
|
||||
+) -> Result<SearchResponse, LoreError> {
|
||||
+ let mode_str = match mode {
|
||||
+ SearchMode::Hybrid => "hybrid",
|
||||
+ SearchMode::Lexical => "lexical",
|
||||
+ SearchMode::Semantic => "semantic",
|
||||
+ };
|
||||
|
||||
@@ 9.3 Phase 0 — Toolchain Gate @@
|
||||
+15. Search mode parity: lexical/hybrid/semantic each return mode-consistent top-N IDs on fixture
|
||||
```
|
||||
|
||||
4. **Guarantee consistent multi-query reads and add query interruption for responsiveness**
|
||||
Analysis: Detail screens combine multiple queries that can observe mixed states during sync writes. Wrap each detail fetch in a single read transaction for snapshot consistency. Add cancellation/interrupt checks for long-running queries so UI remains responsive under heavy datasets.
|
||||
|
||||
```diff
|
||||
@@ 4.5 Async Action System @@
|
||||
+All detail fetches (`issue_detail`, `mr_detail`, timeline expansion) run inside one read transaction
|
||||
+to guarantee snapshot consistency across subqueries.
|
||||
|
||||
@@ 10.11 Action Module — Query Bridge @@
|
||||
+pub fn with_read_snapshot<T>(
|
||||
+ conn: &Connection,
|
||||
+ f: impl FnOnce(&rusqlite::Transaction<'_>) -> Result<T, LoreError>,
|
||||
+) -> Result<T, LoreError> { ... }
|
||||
|
||||
+// Long queries register interrupt checks tied to CancelToken
|
||||
+// to avoid >1s uninterruptible stalls during rapid navigation/filtering.
|
||||
```
|
||||
|
||||
5. **Formalize sync event streaming contract to prevent “stuck” states**
|
||||
Analysis: Dropping events on backpressure is acceptable, but completion must never be dropped and event ordering must be explicit. Add a typed `SyncUiEvent` stream with guaranteed terminal sentinel and progress coalescing to reduce load while preserving correctness.
|
||||
|
||||
```diff
|
||||
@@ 4.4 start_sync_task @@
|
||||
-let (tx, rx) = std::sync::mpsc::sync_channel::<Msg>(1024);
|
||||
+let (tx, rx) = std::sync::mpsc::sync_channel::<SyncUiEvent>(2048);
|
||||
|
||||
-// drop this progress update rather than blocking the sync thread
|
||||
+// coalesce progress to max 30Hz per lane; never drop terminal events
|
||||
+// always emit SyncUiEvent::StreamClosed { outcome }
|
||||
|
||||
@@ 5.9 Sync @@
|
||||
-- Log viewer with streaming output
|
||||
+- Log viewer with streaming output and explicit stream-finalization state
|
||||
+- UI shows dropped/coalesced event counters for transparency
|
||||
```
|
||||
|
||||
6. **Version and validate session restore payloads**
|
||||
Analysis: A raw JSON session file without schema/version checks is fragile across releases and DB switches. Add schema version, DB fingerprint, and safe fallback rules so session restore never blocks startup or applies stale state incorrectly.
|
||||
|
||||
```diff
|
||||
@@ 11. Assumptions @@
|
||||
-12. Minimal TUI state file allowed for session restore only ...
|
||||
+12. Versioned TUI state file allowed for session restore only:
|
||||
+ fields include `schema_version`, `app_version`, `db_fingerprint`, `saved_at`, `state`.
|
||||
|
||||
@@ 10.1 New Files @@
|
||||
crates/lore-tui/src/session.rs # Lightweight session state persistence
|
||||
+ # + versioning, validation, corruption quarantine
|
||||
|
||||
@@ 4.1 Module Structure @@
|
||||
session.rs # Lightweight session state persistence
|
||||
+ # corrupted file -> `.bad-<timestamp>` and fresh start
|
||||
```
|
||||
|
||||
7. **Harden terminal safety beyond ANSI stripping**
|
||||
Analysis: ANSI stripping is necessary but not sufficient. Bidi controls and invisible Unicode controls can still spoof displayed content. URL checks should normalize host/port and disallow deceptive variants. This closes realistic terminal spoofing vectors.
|
||||
|
||||
```diff
|
||||
@@ 3.1 Risk Matrix @@
|
||||
| Terminal escape/control-sequence injection via issue/note text | High | Medium | Strip ANSI/OSC/control chars via sanitize_for_terminal() ... |
|
||||
+| Bidi/invisible Unicode spoofing in rendered text | High | Medium | Strip bidi overrides + zero-width controls in untrusted text |
|
||||
|
||||
@@ 10.4.1 Terminal Safety — Untrusted Text Sanitization @@
|
||||
-Strip ANSI escape sequences, OSC commands, and control characters
|
||||
+Strip ANSI/OSC/control chars, bidi overrides (RLO/LRO/PDF/RLI/LRI/FSI/PDI),
|
||||
+and zero-width/invisible controls from untrusted text
|
||||
|
||||
-pub fn is_safe_url(url: &str, allowed_hosts: &[String]) -> bool {
|
||||
+pub fn is_safe_url(url: &str, allowed_origins: &[Origin]) -> bool {
|
||||
+ // normalize host (IDNA), enforce scheme+host+port match
|
||||
```
|
||||
|
||||
8. **Use progressive hydration for detail screens**
|
||||
Analysis: Issue/MR detail first-paint can become slow when discussions are large. Split fetch into phases: metadata first, then discussions/file changes, then deep thread content on expand. This improves perceived performance and keeps navigation snappy on large repos.
|
||||
|
||||
```diff
|
||||
@@ 5.3 Issue Detail @@
|
||||
-Data source: `lore issues <iid>` + discussions + cross-references
|
||||
+Data source (progressive):
|
||||
+1) metadata/header (first paint)
|
||||
+2) discussions summary + cross-refs
|
||||
+3) full thread bodies loaded on demand when expanded
|
||||
|
||||
@@ 5.5 MR Detail @@
|
||||
-Unique features: File changes list, Diff discussions ...
|
||||
+Unique features (progressive hydration):
|
||||
+- file change summary in first paint
|
||||
+- diff discussion bodies loaded lazily per expanded thread
|
||||
|
||||
@@ 9.3 Phase 0 — Toolchain Gate @@
|
||||
+16. Detail first-paint p95 < 75ms on M-tier fixtures (metadata-only phase)
|
||||
```
|
||||
|
||||
9. **Make reliability tests reproducible with deterministic clocks/seeds**
|
||||
Analysis: Relative-time rendering and fuzz tests are currently tied to wall clock/randomness, which makes CI flakes hard to diagnose. Introduce a `Clock` abstraction and deterministic fuzz seeds with failure replay output.
|
||||
|
||||
```diff
|
||||
@@ 10.9.1 Non-Snapshot Tests @@
|
||||
+/// All time-based rendering uses injected `Clock` in tests.
|
||||
+/// Fuzz failures print deterministic seed for replay.
|
||||
|
||||
@@ 9.2 Phase 5.5 — Reliability Test Pack @@
|
||||
-Event fuzz tests (key/resize/paste):p55g
|
||||
+Event fuzz tests (key/resize/paste, deterministic seed replay):p55g
|
||||
+Deterministic clock/render tests:p55i
|
||||
```
|
||||
|
||||
10. **Add an “Actionable Insights” dashboard panel for stronger day-to-day utility**
|
||||
Analysis: Current dashboard is informative, but not prioritizing. Adding ranked insights (stale P1s, blocked MRs, discussion hotspots) turns it into a decision surface, not just a metrics screen. This makes the TUI materially more compelling for triage workflows.
|
||||
|
||||
```diff
|
||||
@@ 1. Executive Summary @@
|
||||
- Dashboard — sync status, project health, counts at a glance
|
||||
+- Dashboard — sync status, project health, counts, and ranked actionable insights
|
||||
|
||||
@@ 5.1 Dashboard (Home Screen) @@
|
||||
-│ Recent Activity │
|
||||
+│ Recent Activity │
|
||||
+│ Actionable Insights │
|
||||
+│ 1) 7 opened P1 issues >14d │
|
||||
+│ 2) 3 MRs blocked by unresolved │
|
||||
+│ 3) auth/ has +42% note velocity │
|
||||
|
||||
@@ 6. User Flows @@
|
||||
+### 6.9 Flow: "Risk-first morning sweep"
|
||||
+Dashboard -> select insight -> jump to pre-filtered list/detail
|
||||
```
|
||||
|
||||
These 10 changes stay clear of your `Rejected Recommendations` list and materially improve correctness, operability, and product value without adding speculative architecture.
|
||||
150
plans/tui-prd-v2-frankentui.feedback-6.md
Normal file
150
plans/tui-prd-v2-frankentui.feedback-6.md
Normal file
@@ -0,0 +1,150 @@
|
||||
Your plan is strong and unusually detailed. The biggest upgrades I’d make are around build isolation, async correctness, terminal correctness, and turning existing data into sharper triage workflows.
|
||||
|
||||
## 1) Fix toolchain isolation so stable builds cannot accidentally pull nightly
|
||||
Rationale: a `rust-toolchain.toml` inside `crates/lore-tui` is not a complete guard when running workspace commands from repo root. You should structurally prevent stable workflows from touching nightly-only code.
|
||||
|
||||
```diff
|
||||
@@ 3.2 Nightly Rust Strategy
|
||||
-[workspace]
|
||||
-members = [".", "crates/lore-tui"]
|
||||
+[workspace]
|
||||
+members = ["."]
|
||||
+exclude = ["crates/lore-tui"]
|
||||
|
||||
+`crates/lore-tui` is built as an isolated workspace/package with explicit toolchain invocation:
|
||||
+ cargo +nightly-2026-02-08 check --manifest-path crates/lore-tui/Cargo.toml
|
||||
+Core repo remains:
|
||||
+ cargo +stable check --workspace
|
||||
```
|
||||
|
||||
## 2) Add an explicit `lore` <-> `lore-tui` compatibility contract
|
||||
Rationale: runtime delegation is correct, but version drift between binaries will become the #1 support failure mode. Add a handshake before launch.
|
||||
|
||||
```diff
|
||||
@@ 10.19 CLI Integration — Adding `lore tui`
|
||||
+Before spawning `lore-tui`, `lore` runs:
|
||||
+ lore-tui --print-contract-json
|
||||
+and validates:
|
||||
+ - minimum_core_version
|
||||
+ - supported_db_schema_range
|
||||
+ - contract_version
|
||||
+On mismatch, print actionable remediation:
|
||||
+ cargo install --path crates/lore-tui
|
||||
```
|
||||
|
||||
## 3) Make TaskSupervisor truly authoritative (remove split async paths)
|
||||
Rationale: the document says supervisor is the only path, but examples still use direct `Cmd::task` and `search_request_id`. Close that contradiction now to avoid stale-data races.
|
||||
|
||||
```diff
|
||||
@@ 4.4 App — Implementing the Model Trait
|
||||
- search_request_id: u64,
|
||||
+ task_supervisor: TaskSupervisor,
|
||||
|
||||
@@ 4.5.1 Task Supervisor
|
||||
-The `search_request_id` field in `LoreApp` is superseded...
|
||||
+`search_request_id` is removed. All async work uses TaskSupervisor generations.
|
||||
+No direct `Cmd::task` from screen handlers or ad-hoc helpers.
|
||||
```
|
||||
|
||||
## 4) Resolve keybinding conflicts and implement real go-prefix timeout
|
||||
Rationale: `Ctrl+I` collides with `Tab` in terminals. Also your 500ms go-prefix timeout is described but not enforced in code.
|
||||
|
||||
```diff
|
||||
@@ 8.1 Global (Available Everywhere)
|
||||
-| `Ctrl+I` | Jump forward in jump list (entity hops) |
|
||||
+| `Alt+o` | Jump forward in jump list (entity hops) |
|
||||
|
||||
@@ 8.2 Keybinding precedence
|
||||
+Go-prefix timeout is enforced by timestamped state + tick check.
|
||||
+Backspace global-back behavior is implemented (currently documented but not wired).
|
||||
```
|
||||
|
||||
## 5) Add a shared display-width text utility (Unicode-safe truncation and alignment)
|
||||
Rationale: current `truncate()` implementations use byte/char length and will misalign CJK/emoji/full-width text in tables and trees.
|
||||
|
||||
```diff
|
||||
@@ 10.1 New Files
|
||||
+crates/lore-tui/src/text_width.rs # grapheme-safe truncation + display width helpers
|
||||
|
||||
@@ 10.5 Dashboard View / 10.13 Issue List / 10.16 Who View
|
||||
-fn truncate(s: &str, max: usize) -> String { ... }
|
||||
+use crate::text_width::truncate_display_width;
|
||||
+// all column fitting/truncation uses terminal display width, not bytes/chars
|
||||
```
|
||||
|
||||
## 6) Upgrade sync streaming to a QoS event bus with sequence IDs
|
||||
Rationale: today progress/log events can be dropped under load with weak observability. Keep UI responsive while guaranteeing completion semantics and visible gap accounting.
|
||||
|
||||
```diff
|
||||
@@ 4.4 start_sync_task()
|
||||
-let (tx, rx) = std::sync::mpsc::sync_channel::<SyncUiEvent>(2048);
|
||||
+let (ctrl_tx, ctrl_rx) = std::sync::mpsc::sync_channel::<SyncCtrlEvent>(256); // never-drop
|
||||
+let (data_tx, data_rx) = std::sync::mpsc::sync_channel::<SyncDataEvent>(4096); // coalescible
|
||||
|
||||
+Every streamed event carries seq_no.
|
||||
+UI detects gaps and renders: "Dropped N log/progress events due to backpressure."
|
||||
+Terminal events (started/completed/failed/cancelled) remain lossless.
|
||||
```
|
||||
|
||||
## 7) Make list pagination truly keyset-driven in state, not just in prose
|
||||
Rationale: plan text promises windowed keyset paging, but state examples still keep a single list without cursor model. Encode pagination state explicitly.
|
||||
|
||||
```diff
|
||||
@@ 10.10 state/issue_list.rs
|
||||
-pub items: Vec<IssueListRow>,
|
||||
+pub window: Vec<IssueListRow>,
|
||||
+pub next_cursor: Option<IssueCursor>,
|
||||
+pub prev_cursor: Option<IssueCursor>,
|
||||
+pub prefetch: Option<Vec<IssueListRow>>,
|
||||
+pub window_size: usize, // default 200
|
||||
|
||||
@@ 5.2 Issue List
|
||||
-Pagination: Windowed keyset pagination...
|
||||
+Pagination: Keyset cursor model is first-class state with forward/back cursors and prefetch buffer.
|
||||
```
|
||||
|
||||
## 8) Harden session restore with atomic persistence + integrity checksum
|
||||
Rationale: versioning/quarantine is good, but you still need crash-safe write semantics and tamper/corruption detection to avoid random boot failures.
|
||||
|
||||
```diff
|
||||
@@ 10.1 New Files
|
||||
-crates/lore-tui/src/session.rs # Versioned session state persistence + validation + corruption quarantine
|
||||
+crates/lore-tui/src/session.rs # + atomic write (tmp->fsync->rename), checksum, max-size guard
|
||||
|
||||
@@ 11. Assumptions
|
||||
+Session writes are atomic and checksummed.
|
||||
+Invalid checksum or oversized file triggers quarantine and fresh boot.
|
||||
```
|
||||
|
||||
## 9) Evolve Doctor from read-only text into actionable remediation
|
||||
Rationale: your CLI already returns machine-actionable `actions`. TUI should surface those as one-key fixes; this materially increases usefulness.
|
||||
|
||||
```diff
|
||||
@@ 5.11 Doctor / Stats (Info Screens)
|
||||
-Simple read-only views rendering the output...
|
||||
+Doctor is interactive:
|
||||
+ - shows health checks + severity
|
||||
+ - exposes suggested `actions` from robot-mode errors
|
||||
+ - Enter runs selected action command (with confirmation modal)
|
||||
+Stats remains read-only.
|
||||
```
|
||||
|
||||
## 10) Add a Dependency Lens to Issue/MR detail (high-value triage feature)
|
||||
Rationale: you already have cross-refs + discussions + timeline. A compact dependency panel (blocked-by / blocks / unresolved threads) makes this data operational for prioritization.
|
||||
|
||||
```diff
|
||||
@@ 5.3 Issue Detail
|
||||
-│ ┌─ Cross-References ─────────────────────────────────────────┐ │
|
||||
+│ ┌─ Dependency Lens ──────────────────────────────────────────┐ │
|
||||
+│ │ Blocked by: #1198 (open, stale 9d) │ │
|
||||
+│ │ Blocks: !458 (opened, 2 unresolved threads) │ │
|
||||
+│ │ Risk: High (P1 + stale blocker + open MR discussion) │ │
|
||||
+│ └────────────────────────────────────────────────────────────┘ │
|
||||
|
||||
@@ 9.2 Phases
|
||||
+Dependency Lens (issue/mr detail, computed risk score) :p3e, after p2e, 1d
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If you want, I can next produce a consolidated **“v2.1 patch”** of the PRD with all these edits merged into one coherent updated document structure.
|
||||
264
plans/tui-prd-v2-frankentui.feedback-7.md
Normal file
264
plans/tui-prd-v2-frankentui.feedback-7.md
Normal file
@@ -0,0 +1,264 @@
|
||||
1. **Fix a critical contradiction in workspace/toolchain isolation**
|
||||
Rationale: Section `3.2` says `crates/lore-tui` is excluded from the root workspace, but Section `9.1` currently adds it as a member. That inconsistency will cause broken CI/tooling behavior and confusion about whether stable-only workflows remain safe.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 9.1 Dependency Changes
|
||||
-# Root Cargo.toml changes
|
||||
-[workspace]
|
||||
-members = [".", "crates/lore-tui"]
|
||||
+# Root Cargo.toml changes
|
||||
+[workspace]
|
||||
+members = ["."]
|
||||
+exclude = ["crates/lore-tui"]
|
||||
@@
|
||||
-# Add workspace member (no lore-tui dep, no tui feature)
|
||||
+# Keep lore-tui EXCLUDED from root workspace (nightly isolation boundary)
|
||||
@@ 9.3 Phase 0 — Toolchain Gate
|
||||
-1. `cargo check --all-targets` passes on pinned nightly (TUI crate) and stable (core)
|
||||
+1. `cargo +stable check --workspace --all-targets` passes for root workspace
|
||||
+2. `cargo +nightly-2026-02-08 check --manifest-path crates/lore-tui/Cargo.toml --all-targets` passes
|
||||
```
|
||||
|
||||
2. **Replace global loading spinner with per-screen stale-while-revalidate**
|
||||
Rationale: A single `is_loading` flag causes full-screen flicker and blocked context during quick refreshes. Per-screen load states keep existing data visible while background refresh runs, improving perceived performance and usability.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 10.10 State Module — Complete
|
||||
- pub is_loading: bool,
|
||||
+ pub load_state: ScreenLoadStateMap,
|
||||
@@
|
||||
- pub fn set_loading(&mut self, loading: bool) {
|
||||
- self.is_loading = loading;
|
||||
- }
|
||||
+ pub fn set_loading(&mut self, screen: ScreenId, state: LoadState) {
|
||||
+ self.load_state.insert(screen, state);
|
||||
+ }
|
||||
+
|
||||
+pub enum LoadState {
|
||||
+ Idle,
|
||||
+ LoadingInitial,
|
||||
+ Refreshing, // stale data remains visible
|
||||
+ Error(String),
|
||||
+}
|
||||
@@ 4.4 App — Implementing the Model Trait
|
||||
- // Loading spinner overlay (while async data is fetching)
|
||||
- if self.state.is_loading {
|
||||
- crate::tui::view::common::render_loading(frame, body);
|
||||
- } else {
|
||||
- match self.navigation.current() { ... }
|
||||
- }
|
||||
+ // Always render screen; show lightweight refresh indicator when needed.
|
||||
+ match self.navigation.current() { ... }
|
||||
+ crate::tui::view::common::render_refresh_indicator_if_needed(
|
||||
+ self.navigation.current(), &self.state.load_state, frame, body
|
||||
+ );
|
||||
```
|
||||
|
||||
3. **Make `TaskSupervisor` a real scheduler (not just token registry)**
|
||||
Rationale: Current design declares priority lanes but still dispatches directly with `Cmd::task`, and debounce uses `thread::sleep` per keystroke (wastes worker threads). A bounded scheduler with queued tasks and timer-driven debounce will reduce contention and tail latency.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 4.5.1 Task Supervisor (Dedup + Cancellation + Priority)
|
||||
-pub struct TaskSupervisor {
|
||||
- active: HashMap<TaskKey, Arc<CancelToken>>,
|
||||
- generation: AtomicU64,
|
||||
-}
|
||||
+pub struct TaskSupervisor {
|
||||
+ active: HashMap<TaskKey, Arc<CancelToken>>,
|
||||
+ generation: AtomicU64,
|
||||
+ queue: BinaryHeap<ScheduledTask>,
|
||||
+ inflight: HashMap<TaskPriority, usize>,
|
||||
+ limits: TaskLaneLimits, // e.g. Input=4, Navigation=2, Background=1
|
||||
+}
|
||||
@@
|
||||
-// 200ms debounce via cancelable scheduled event (not thread::sleep).
|
||||
-Cmd::task(move || {
|
||||
- std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
- ...
|
||||
-})
|
||||
+// Debounce via runtime timer message; no sleeping worker thread.
|
||||
+self.state.search.debounce_deadline = Some(now + 200ms);
|
||||
+Cmd::none()
|
||||
@@ 4.4 update()
|
||||
+Msg::Tick => {
|
||||
+ if self.state.search.debounce_expired(now) {
|
||||
+ return self.dispatch_supervised(TaskKey::Search, TaskPriority::Input, ...);
|
||||
+ }
|
||||
+ self.task_supervisor.dispatch_ready(now)
|
||||
+}
|
||||
```
|
||||
|
||||
4. **Add a sync run ledger for exact “new since sync” navigation**
|
||||
Rationale: “Since last sync” based on timestamps is ambiguous with partial failures, retries, and clock drift. A lightweight `sync_runs` + `sync_deltas` ledger makes summary-mode drill-down exact and auditable without implementing full resumable checkpoints.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 5.9 Sync
|
||||
-- `i` navigates to Issue List pre-filtered to "since last sync"
|
||||
-- `m` navigates to MR List pre-filtered to "since last sync"
|
||||
+- `i` navigates to Issue List pre-filtered to `sync_run_id=<last_run>`
|
||||
+- `m` navigates to MR List pre-filtered to `sync_run_id=<last_run>`
|
||||
+- Filters are driven by persisted `sync_deltas` rows (exact entity keys changed in run)
|
||||
@@ 10.1 New Files
|
||||
+src/core/migrations/00xx_add_sync_run_ledger.sql
|
||||
@@ New migration (appendix)
|
||||
+CREATE TABLE sync_runs (
|
||||
+ id INTEGER PRIMARY KEY,
|
||||
+ started_at_ms INTEGER NOT NULL,
|
||||
+ completed_at_ms INTEGER,
|
||||
+ status TEXT NOT NULL
|
||||
+);
|
||||
+CREATE TABLE sync_deltas (
|
||||
+ sync_run_id INTEGER NOT NULL,
|
||||
+ entity_kind TEXT NOT NULL,
|
||||
+ project_id INTEGER NOT NULL,
|
||||
+ iid INTEGER NOT NULL,
|
||||
+ change_kind TEXT NOT NULL
|
||||
+);
|
||||
+CREATE INDEX idx_sync_deltas_run_kind ON sync_deltas(sync_run_id, entity_kind);
|
||||
@@ 11 Assumptions
|
||||
-16. No new SQLite tables needed for v1
|
||||
+16. Two small v1 tables are added: `sync_runs` and `sync_deltas` for deterministic post-sync UX.
|
||||
```
|
||||
|
||||
5. **Expand the GA index set to match actual filter surface**
|
||||
Rationale: Current required indexes only cover default sort paths; they do not match common filters like `author`, `assignee`, `reviewer`, `target_branch`, label-based filtering. This will likely miss p95 SLOs at M tier.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 9.3.1 Required Indexes (GA Blocker)
|
||||
CREATE INDEX IF NOT EXISTS idx_issues_list_default
|
||||
ON issues(project_id, state, updated_at DESC, iid DESC);
|
||||
+CREATE INDEX IF NOT EXISTS idx_issues_author_updated
|
||||
+ ON issues(project_id, state, author_username, updated_at DESC, iid DESC);
|
||||
+CREATE INDEX IF NOT EXISTS idx_issues_assignee_updated
|
||||
+ ON issues(project_id, state, assignee_username, updated_at DESC, iid DESC);
|
||||
@@
|
||||
CREATE INDEX IF NOT EXISTS idx_mrs_list_default
|
||||
ON merge_requests(project_id, state, updated_at DESC, iid DESC);
|
||||
+CREATE INDEX IF NOT EXISTS idx_mrs_reviewer_updated
|
||||
+ ON merge_requests(project_id, state, reviewer_username, updated_at DESC, iid DESC);
|
||||
+CREATE INDEX IF NOT EXISTS idx_mrs_target_updated
|
||||
+ ON merge_requests(project_id, state, target_branch, updated_at DESC, iid DESC);
|
||||
+CREATE INDEX IF NOT EXISTS idx_mrs_source_updated
|
||||
+ ON merge_requests(project_id, state, source_branch, updated_at DESC, iid DESC);
|
||||
@@
|
||||
+-- If labels are normalized through join table:
|
||||
+CREATE INDEX IF NOT EXISTS idx_issue_labels_label_issue ON issue_labels(label, issue_id);
|
||||
+CREATE INDEX IF NOT EXISTS idx_mr_labels_label_mr ON mr_labels(label, mr_id);
|
||||
@@ CI enforcement
|
||||
-asserts that none show `SCAN TABLE` for the primary entity tables
|
||||
+asserts that none show full scans for primary tables under default filters AND top 8 user-facing filter combinations
|
||||
```
|
||||
|
||||
6. **Add DB schema compatibility preflight (separate from binary compat)**
|
||||
Rationale: Binary compat (`--compat-version`) does not protect against schema mismatches. Add explicit schema version checks before booting the TUI to avoid runtime SQL errors deep in navigation paths.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 3.2 Nightly Rust Strategy
|
||||
-- **Compatibility contract:** Before spawning `lore-tui`, the `lore tui` subcommand runs `lore-tui --compat-version` ...
|
||||
+- **Compatibility contract:** Before spawning `lore-tui`, `lore tui` validates:
|
||||
+ 1) binary compat version (`lore-tui --compat-version`)
|
||||
+ 2) DB schema range (`lore-tui --check-schema <db-path>`)
|
||||
+If schema is out-of-range, print remediation: `lore migrate`.
|
||||
@@ 9.3 Phase 0 — Toolchain Gate
|
||||
+17. Schema preflight test: incompatible DB schema yields actionable error and non-zero exit before entering TUI loop.
|
||||
```
|
||||
|
||||
7. **Refine terminal sanitization to preserve legitimate Unicode while blocking control attacks**
|
||||
Rationale: Current sanitizer strips zero-width joiners and similar characters, which breaks emoji/grapheme rendering and undermines your own `text_width` goals. Keep benign Unicode, remove only dangerous controls/bidi spoof vectors, and sanitize markdown link targets too.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 10.4.1 Terminal Safety — Untrusted Text Sanitization
|
||||
-- Strip bidi overrides ... and zero-width/invisible controls ...
|
||||
+- Strip ANSI/OSC/control chars and bidi spoof controls.
|
||||
+- Preserve legitimate grapheme-joining characters (ZWJ/ZWNJ/combining marks) for correct Unicode rendering.
|
||||
+- Sanitize markdown link targets with strict URL allowlist before rendering clickable links.
|
||||
@@ safety.rs
|
||||
- // Strip zero-width and invisible controls
|
||||
- '\u{200B}' | '\u{200C}' | '\u{200D}' | '\u{FEFF}' | '\u{00AD}' => {}
|
||||
+ // Preserve grapheme/emoji join behavior; remove only harmful controls.
|
||||
+ // (ZWJ/ZWNJ/combining marks are retained)
|
||||
@@ Enforcement rule
|
||||
- Search result snippets
|
||||
- Author names and labels
|
||||
+- Markdown link destinations (scheme + origin validation before render/open)
|
||||
```
|
||||
|
||||
8. **Add key normalization layer for terminal portability**
|
||||
Rationale: Collision notes are good, but you still need a canonicalization layer because terminals emit different sequences for Alt/Meta/Backspace/Enter variants. This reduces “works in iTerm, broken in tmux/SSH” bugs.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 8.2 List Screens
|
||||
**Terminal keybinding safety notes:**
|
||||
@@
|
||||
- `Ctrl+M` is NOT used — it collides with `Enter` ...
|
||||
+
|
||||
+**Key normalization layer (new):**
|
||||
+- Introduce `KeyNormalizer` before `interpret_key()`:
|
||||
+ - normalize Backspace variants (`^H`, `DEL`)
|
||||
+ - normalize Alt/Meta prefixes
|
||||
+ - normalize Shift+Tab vs Tab where terminal supports it
|
||||
+ - normalize kitty/CSI-u enhanced key protocols when present
|
||||
@@ 9.2 Phases
|
||||
+ Key normalization integration tests :p5d, after p5c, 1d
|
||||
+ Terminal profile replay tests :p5e, after p5d, 1d
|
||||
```
|
||||
|
||||
9. **Add deterministic event-trace capture for crash reproduction**
|
||||
Rationale: Panic logs without recent event context are often insufficient for TUI race bugs. Persist last-N normalized events + active screen + task state snapshot on panic for one-command repro.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 3.1 Risk Matrix
|
||||
| Runtime panic leaves user blocked | High | Medium | Panic hook writes crash report, restores terminal, offers fallback CLI command |
|
||||
+| Hard-to-reproduce input race bugs | Medium | Medium | Persist last 2k normalized events + state hash on panic for deterministic replay |
|
||||
@@ 10.3 Entry Point / panic hook
|
||||
- // 2. Write crash dump
|
||||
+ // 2. Write crash dump + event trace snapshot
|
||||
+ // Includes: last 2000 normalized events, current screen, in-flight task keys/generations
|
||||
@@ 10.9.1 Non-Snapshot Tests
|
||||
+/// Replay captured event trace from panic artifact and assert no panic.
|
||||
+#[test]
|
||||
+fn replay_trace_artifact_is_stable() { ... }
|
||||
```
|
||||
|
||||
10. **Do a plan-wide consistency pass on pseudocode contracts**
|
||||
Rationale: There are internal mismatches that will create implementation churn (`search_request_id` still referenced after replacement, `items` vs `window`, keybinding mismatch `Ctrl+I` vs `Alt+o`). Tightening these now saves real engineering time later.
|
||||
|
||||
```diff
|
||||
--- a/PRD.md
|
||||
+++ b/PRD.md
|
||||
@@ 4.4 LoreApp::new
|
||||
- search_request_id: 0,
|
||||
+ // dedup generation handled by TaskSupervisor
|
||||
@@ 8.1 Global
|
||||
-| `Ctrl+O` | Jump backward in jump list (entity hops) |
|
||||
-| `Alt+o` | Jump forward in jump list (entity hops) |
|
||||
+| `Ctrl+O` | Jump backward in jump list (entity hops) |
|
||||
+| `Alt+o` | Jump forward in jump list (entity hops) |
|
||||
@@ 10.10 IssueListState
|
||||
- pub fn selected_item(&self) -> Option<&IssueListRow> {
|
||||
- self.items.get(self.selected_index)
|
||||
- }
|
||||
+ pub fn selected_item(&self) -> Option<&IssueListRow> {
|
||||
+ self.window.get(self.selected_index)
|
||||
+ }
|
||||
```
|
||||
|
||||
If you want, I can now produce a single consolidated unified diff patch of the full PRD with these revisions merged end-to-end.
|
||||
211
plans/tui-prd-v2-frankentui.feedback-8.md
Normal file
211
plans/tui-prd-v2-frankentui.feedback-8.md
Normal file
@@ -0,0 +1,211 @@
|
||||
Below are the strongest revisions I’d make. I intentionally avoided anything in your `## Rejected Recommendations`.
|
||||
|
||||
1. **Unify commands/keybindings/help/palette into one registry**
|
||||
Rationale: your plan currently duplicates action definitions across `execute_palette_action`, `ShowCliEquivalent`, help overlay text, and status hints. That will drift quickly and create correctness bugs. A single `CommandRegistry` makes behavior consistent and testable.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 4.1 Module Structure
|
||||
+ commands.rs # Single source of truth for actions, keybindings, CLI equivalents
|
||||
|
||||
@@ 4.4 App — Implementing the Model Trait
|
||||
- fn execute_palette_action(&self, action_id: &str) -> Cmd<Msg> { ... big match ... }
|
||||
+ fn execute_palette_action(&self, action_id: &str) -> Cmd<Msg> {
|
||||
+ if let Some(spec) = self.commands.get(action_id) {
|
||||
+ return self.update(spec.to_msg(self.navigation.current()));
|
||||
+ }
|
||||
+ Cmd::none()
|
||||
+ }
|
||||
|
||||
@@ 8. Keybinding Reference
|
||||
+All keybinding/help/status/palette definitions are generated from `commands.rs`.
|
||||
+No hardcoded duplicate maps in view/state modules.
|
||||
```
|
||||
|
||||
2. **Replace ad-hoc key flags with explicit input state machine**
|
||||
Rationale: `pending_go` + `go_prefix_instant` is fragile and already inconsistent with documented behavior. A typed `InputMode` removes edge-case bugs and makes prefix timeout deterministic.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 4.4 LoreApp struct
|
||||
- pending_go: bool,
|
||||
- go_prefix_instant: Option<std::time::Instant>,
|
||||
+ input_mode: InputMode, // Normal | Text | Palette | GoPrefix { started_at }
|
||||
|
||||
@@ 8.2 List Screens
|
||||
-| `g` `g` | Jump to top |
|
||||
+| `g` `g` | Jump to top (current list screen) |
|
||||
|
||||
@@ 4.4 interpret_key
|
||||
- KeyCode::Char('g') => Msg::IssueListScrollToTop
|
||||
+ KeyCode::Char('g') => Msg::ScrollToTopCurrentScreen
|
||||
```
|
||||
|
||||
3. **Fix TaskSupervisor contract and message schema drift**
|
||||
Rationale: the plan mixes `request_id` and `generation`, and `TaskKey::Search { generation }` defeats dedup by making every key unique. This can silently reintroduce stale-result races.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 4.3 Core Types (Msg)
|
||||
- SearchRequestStarted { request_id: u64, query: String },
|
||||
- SearchExecuted { request_id: u64, results: SearchResults },
|
||||
+ SearchRequestStarted { generation: u64, query: String },
|
||||
+ SearchExecuted { generation: u64, results: SearchResults },
|
||||
|
||||
@@ 4.5.1 Task Supervisor
|
||||
- Search { generation: u64 },
|
||||
+ Search,
|
||||
+ struct TaskStamp { key: TaskKey, generation: u64 }
|
||||
|
||||
@@ 10.9.1 Non-Snapshot Tests
|
||||
- Msg::SearchExecuted { request_id: 3, ... }
|
||||
+ Msg::SearchExecuted { generation: 3, ... }
|
||||
```
|
||||
|
||||
4. **Add a `Clock` boundary everywhere time is computed**
|
||||
Rationale: you call `SystemTime::now()` in many query/render paths, causing inconsistent relative-time labels inside one frame and flaky tests. Injected clock gives deterministic rendering and lower per-frame overhead.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 4.1 Module Structure
|
||||
+ clock.rs # Clock trait: SystemClock/FakeClock
|
||||
|
||||
@@ 4.4 LoreApp struct
|
||||
+ clock: Arc<dyn Clock>,
|
||||
|
||||
@@ 10.11 action.rs
|
||||
- let now_ms = std::time::SystemTime::now()...
|
||||
+ let now_ms = clock.now_ms();
|
||||
|
||||
@@ 9.3 Phase 0 success criteria
|
||||
+19. Relative-time rendering deterministic under FakeClock across snapshot runs.
|
||||
```
|
||||
|
||||
5. **Upgrade text truncation to grapheme-safe width handling**
|
||||
Rationale: `unicode-width` alone is not enough for safe truncation; it can split grapheme clusters (emoji ZWJ sequences, skin tones, flags). You need width + grapheme segmentation together.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 10.1 New Files
|
||||
-crates/lore-tui/src/text_width.rs # ... using unicode-width crate
|
||||
+crates/lore-tui/src/text_width.rs # Grapheme-safe width/truncation using unicode-width + unicode-segmentation
|
||||
|
||||
@@ 10.1 New Files
|
||||
+Cargo.toml (lore-tui): unicode-segmentation = "1"
|
||||
|
||||
@@ 9.3 Phase 0 success criteria
|
||||
+20. Unicode rendering tests pass for CJK, emoji ZWJ, combining marks, RTL text.
|
||||
```
|
||||
|
||||
6. **Redact sensitive values in logs and crash dumps**
|
||||
Rationale: current crash/log strategy risks storing tokens/credentials in plain text. This is a serious operational/security gap for local tooling too.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 4.1 Module Structure
|
||||
safety.rs # sanitize_for_terminal(), safe_url_policy()
|
||||
+ redact.rs # redact_sensitive() for logs/crash reports
|
||||
|
||||
@@ 10.3 install_panic_hook_for_tui
|
||||
- let _ = std::fs::write(&crash_path, format!("{panic_info:#?}"));
|
||||
+ let report = redact_sensitive(format!("{panic_info:#?}"));
|
||||
+ let _ = std::fs::write(&crash_path, report);
|
||||
|
||||
@@ 9.3 Phase 0 success criteria
|
||||
+21. Redaction tests confirm tokens/Authorization headers never appear in persisted crash/log artifacts.
|
||||
```
|
||||
|
||||
7. **Add search capability detection and mode fallback UX**
|
||||
Rationale: semantic/hybrid mode should not silently degrade when embeddings are absent/stale. Explicit capability state increases trust and avoids “why are results weird?” confusion.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 5.6 Search
|
||||
+Capability-aware modes:
|
||||
+- If embeddings unavailable/stale, semantic mode is disabled with inline reason.
|
||||
+- Hybrid mode auto-falls back to lexical and shows badge: "semantic unavailable".
|
||||
|
||||
@@ 4.3 Core Types
|
||||
+ SearchCapabilitiesLoaded(SearchCapabilities)
|
||||
|
||||
@@ 9.3 Phase 0 success criteria
|
||||
+22. Mode availability checks validated: lexical/hybrid/semantic correctly enabled/disabled by fixture capabilities.
|
||||
```
|
||||
|
||||
8. **Define sync cancel latency SLO and enforce fine-grained checks**
|
||||
Rationale: “check cancel between phases” is too coarse on big projects. Users need fast cancel acknowledgment and bounded stop time.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 5.9 Sync
|
||||
-CANCELLATION: checked between sync phases
|
||||
+CANCELLATION: checked at page boundaries, batch upsert boundaries, and before each network request.
|
||||
+UX target: cancel acknowledged <250ms, sync stop p95 <2s after Esc.
|
||||
|
||||
@@ 9.3 Phase 0 success criteria
|
||||
+23. Cancel latency test passes: p95 stop time <2s under M-tier fixtures.
|
||||
```
|
||||
|
||||
9. **Add a “Hotspots” screen for risk/churn triage**
|
||||
Rationale: this is high-value and uses existing data (events, unresolved discussions, stale items). It makes the TUI more compelling without needing new sync tables or rejected features.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 1. Executive Summary
|
||||
+- **Hotspots** — file/path risk ranking by churn × unresolved discussion pressure × staleness
|
||||
|
||||
@@ 5. Screen Taxonomy
|
||||
+### 5.12 Hotspots
|
||||
+Shows top risky paths with drill-down to related issues/MRs/timeline.
|
||||
|
||||
@@ 8.1 Global
|
||||
+| `gx` | Go to Hotspots |
|
||||
|
||||
@@ 10.1 New Files
|
||||
+crates/lore-tui/src/state/hotspots.rs
|
||||
+crates/lore-tui/src/view/hotspots.rs
|
||||
```
|
||||
|
||||
10. **Add degraded startup mode when compat/schema checks fail**
|
||||
Rationale: hard-exit on mismatch blocks users. A degraded mode that shells to `lore --robot` for read-only summary/doctor keeps the product usable and gives guided recovery.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 3.2 Nightly Rust Strategy
|
||||
- On mismatch: actionable error and exit
|
||||
+ On mismatch: actionable error with `--degraded` option.
|
||||
+ `--degraded` launches limited TUI (Dashboard/Doctor/Stats via `lore --robot` subprocess calls).
|
||||
|
||||
@@ 10.3 TuiCli
|
||||
+ /// Allow limited mode when schema/compat checks fail
|
||||
+ #[arg(long)]
|
||||
+ degraded: bool,
|
||||
```
|
||||
|
||||
11. **Harden query-plan CI checks (don’t rely on `SCAN TABLE` string matching)**
|
||||
Rationale: SQLite planner text varies by version. Parse opcode structure and assert index usage semantically; otherwise CI will be flaky or miss regressions.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 9.3.1 Required Indexes (CI enforcement)
|
||||
- asserts that none show `SCAN TABLE`
|
||||
+ parses EXPLAIN QUERY PLAN rows and asserts:
|
||||
+ - top-level loop uses expected index families
|
||||
+ - no full scan on primary entity tables under default and top filter combos
|
||||
+ - join order remains bounded (no accidental cartesian expansions)
|
||||
```
|
||||
|
||||
12. **Enforce single-instance lock for session/state safety**
|
||||
Rationale: assumption says no concurrent TUI sessions, but accidental double-launch will still happen. Locking prevents state corruption and confusing interleaved sync actions.
|
||||
|
||||
```diff
|
||||
diff --git a/PRD.md b/PRD.md
|
||||
@@ 10.1 New Files
|
||||
+crates/lore-tui/src/instance_lock.rs # lock file with stale-lock recovery
|
||||
|
||||
@@ 11. Assumptions
|
||||
-21. No concurrent TUI sessions.
|
||||
+21. Concurrent sessions unsupported and actively prevented by instance lock (with clear error message).
|
||||
```
|
||||
|
||||
If you want, I can turn this into a consolidated patched PRD (single unified diff) next.
|
||||
198
plans/tui-prd-v2-frankentui.feedback-9.md
Normal file
198
plans/tui-prd-v2-frankentui.feedback-9.md
Normal file
@@ -0,0 +1,198 @@
|
||||
I reviewed the full PRD end-to-end and avoided all items already listed in `## Rejected Recommendations`.
|
||||
These are the highest-impact revisions I’d make.
|
||||
|
||||
1. **Fix keybinding/state-machine correctness gaps (critical)**
|
||||
The plan currently has an internal conflict: the doc says jump-forward is `Alt+o`, but code sample uses `Ctrl+i` (which collides with `Tab` in many terminals). Also, `g`-prefix timeout depends on `Tick`, but `Tick` isn’t guaranteed when idle, so prefix mode can get “stuck.” This is a correctness bug, not polish.
|
||||
|
||||
```diff
|
||||
@@ 8.1 Global (Available Everywhere)
|
||||
-| `Ctrl+O` | Jump backward in jump list (entity hops) |
|
||||
-| `Alt+o` | Jump forward in jump list (entity hops) |
|
||||
+| `Ctrl+O` | Jump backward in jump list (entity hops) |
|
||||
+| `Alt+o` | Jump forward in jump list (entity hops) |
|
||||
+| `Backspace` | Go back (when no text input is focused) |
|
||||
|
||||
@@ 4.4 LoreApp::interpret_key
|
||||
- (KeyCode::Char('i'), m) if m.contains(Modifiers::CTRL) => {
|
||||
- return Some(Msg::JumpForward);
|
||||
- }
|
||||
+ (KeyCode::Char('o'), m) if m.contains(Modifiers::ALT) => {
|
||||
+ return Some(Msg::JumpForward);
|
||||
+ }
|
||||
+ (KeyCode::Backspace, Modifiers::NONE) => {
|
||||
+ return Some(Msg::GoBack);
|
||||
+ }
|
||||
|
||||
@@ 4.4 Model::subscriptions
|
||||
+ // Go-prefix timeout enforcement must tick even when nothing is loading.
|
||||
+ if matches!(self.input_mode, InputMode::GoPrefix { .. }) {
|
||||
+ subs.push(Box::new(
|
||||
+ Every::with_id(2, Duration::from_millis(50), || Msg::Tick)
|
||||
+ ));
|
||||
+ }
|
||||
```
|
||||
|
||||
2. **Make `TaskSupervisor` API internally consistent and enforceable**
|
||||
The plan uses `submit()`/`is_current()` in one place and `register()`/`next_generation()` in another. That inconsistency will cause implementation drift and stale-result bugs. Use one coherent API with a returned handle containing `{key, generation, cancel_token}`.
|
||||
|
||||
```diff
|
||||
@@ 4.5.1 Task Supervisor (Dedup + Cancellation + Priority)
|
||||
-pub struct TaskSupervisor {
|
||||
- active: HashMap<TaskKey, Arc<CancelToken>>,
|
||||
- generation: AtomicU64,
|
||||
-}
|
||||
+pub struct TaskSupervisor {
|
||||
+ active: HashMap<TaskKey, TaskHandle>,
|
||||
+}
|
||||
+
|
||||
+pub struct TaskHandle {
|
||||
+ pub key: TaskKey,
|
||||
+ pub generation: u64,
|
||||
+ pub cancel: Arc<CancelToken>,
|
||||
+}
|
||||
|
||||
- pub fn register(&mut self, key: TaskKey) -> Arc<CancelToken>
|
||||
- pub fn next_generation(&self) -> u64
|
||||
+ pub fn submit(&mut self, key: TaskKey) -> TaskHandle
|
||||
+ pub fn is_current(&self, key: &TaskKey, generation: u64) -> bool
|
||||
+ pub fn complete(&mut self, key: &TaskKey, generation: u64)
|
||||
```
|
||||
|
||||
3. **Replace thread-sleep debounce with runtime timer messages**
|
||||
`std::thread::sleep(200ms)` inside task closures wastes pool threads under fast typing and reduces responsiveness under contention. Use timer-driven debounce messages and only fire the latest generation. This improves latency stability on large datasets.
|
||||
|
||||
```diff
|
||||
@@ 4.3 Core Types (Msg enum)
|
||||
+ SearchDebounceArmed { generation: u64, query: String },
|
||||
+ SearchDebounceFired { generation: u64 },
|
||||
|
||||
@@ 4.4 maybe_debounced_query
|
||||
- Cmd::task(move || {
|
||||
- std::thread::sleep(std::time::Duration::from_millis(200));
|
||||
- ...
|
||||
- })
|
||||
+ // Arm debounce only; runtime timer emits SearchDebounceFired.
|
||||
+ Cmd::msg(Msg::SearchDebounceArmed { generation, query })
|
||||
|
||||
@@ 4.4 subscriptions()
|
||||
+ if self.state.search.debounce_pending() {
|
||||
+ subs.push(Box::new(
|
||||
+ Every::with_id(3, Duration::from_millis(200), || Msg::SearchDebounceFired { generation: ... })
|
||||
+ ));
|
||||
+ }
|
||||
```
|
||||
|
||||
4. **Harden `DbManager` API to avoid lock-poison panics and accidental long-held guards**
|
||||
Returning raw `MutexGuard<Connection>` invites accidental lock scope expansion and `expect("lock poisoned")` panics. Move to closure-based access (`with_reader`, `with_writer`) returning `Result`, and use cached statements. This reduces deadlock risk and tail latency.
|
||||
|
||||
```diff
|
||||
@@ 4.4 DbManager
|
||||
- pub fn reader(&self) -> MutexGuard<'_, Connection> { ...expect("reader lock poisoned") }
|
||||
- pub fn writer(&self) -> MutexGuard<'_, Connection> { ...expect("writer lock poisoned") }
|
||||
+ pub fn with_reader<T>(&self, f: impl FnOnce(&Connection) -> Result<T, LoreError>) -> Result<T, LoreError>
|
||||
+ pub fn with_writer<T>(&self, f: impl FnOnce(&Connection) -> Result<T, LoreError>) -> Result<T, LoreError>
|
||||
|
||||
@@ 10.11 action.rs
|
||||
- let conn = db.reader();
|
||||
- match fetch_issues(&conn, &filter) { ... }
|
||||
+ match db.with_reader(|conn| fetch_issues(conn, &filter)) { ... }
|
||||
|
||||
+ // Query hot paths use prepare_cached() to reduce parse overhead.
|
||||
```
|
||||
|
||||
5. **Add read-path entity cache (LRU) for repeated drill-in/out workflows**
|
||||
Your core daily flow is Enter/Esc bouncing between list/detail. Without caching, identical detail payloads are re-queried repeatedly. A bounded LRU by `EntityKey` with invalidation on sync completion gives near-instant reopen behavior and reduces DB pressure.
|
||||
|
||||
```diff
|
||||
@@ 4.1 Module Structure
|
||||
+ entity_cache.rs # Bounded LRU cache for detail payloads
|
||||
|
||||
@@ app.rs LoreApp fields
|
||||
+ entity_cache: EntityCache,
|
||||
|
||||
@@ load_screen(Screen::IssueDetail / MrDetail)
|
||||
+ if let Some(cached) = self.entity_cache.get_issue(&key) {
|
||||
+ return Cmd::msg(Msg::IssueDetailLoaded { key, detail: cached.clone() });
|
||||
+ }
|
||||
|
||||
@@ Msg::IssueDetailLoaded / Msg::MrDetailLoaded handlers
|
||||
+ self.entity_cache.put_issue(key.clone(), detail.clone());
|
||||
|
||||
@@ Msg::SyncCompleted
|
||||
+ self.entity_cache.invalidate_all();
|
||||
```
|
||||
|
||||
6. **Tighten sync-stream observability and drop semantics without adding heavy architecture**
|
||||
You already handle backpressure, but operators need visibility when it happens. Track dropped-progress count and max queue depth in state and surface it in running/summary views. This keeps the current simple design while making reliability measurable.
|
||||
|
||||
```diff
|
||||
@@ 4.3 Msg
|
||||
+ SyncStreamStats { dropped_progress: u64, max_queue_depth: usize },
|
||||
|
||||
@@ 5.9 Sync (Running mode footer)
|
||||
-| Esc cancel f full sync e embed after d dry-run l log level|
|
||||
+| Esc cancel f full sync e embed after d dry-run l log level stats:drop=12 qmax=1847 |
|
||||
|
||||
@@ 9.3 Success criteria
|
||||
+24. Sync stream stats are emitted and rendered; terminal events (completed/failed/cancelled) delivery is 100% under induced backpressure.
|
||||
```
|
||||
|
||||
7. **Make crash reporting match the promised diagnostic value**
|
||||
The PRD promises event replay context, but sample hook writes only panic text. Add explicit crash context capture (`last events`, `current screen`, `task handles`, `build id`, `db fingerprint`) and retention policy. This materially improves post-mortem debugging.
|
||||
|
||||
```diff
|
||||
@@ 4.1 Module Structure
|
||||
+ crash_context.rs # ring buffer of normalized events + task/screen snapshot
|
||||
|
||||
@@ 10.3 install_panic_hook_for_tui()
|
||||
- let report = crate::redact::redact_sensitive(&format!("{panic_info:#?}"));
|
||||
+ let ctx = crate::crash_context::snapshot();
|
||||
+ let report = crate::redact::redact_sensitive(&format!("{panic_info:#?}\n{ctx:#?}"));
|
||||
|
||||
+ // Retention: keep latest 20 crash files, delete oldest metadata entries only.
|
||||
```
|
||||
|
||||
8. **Add Search Facets panel for faster triage (high-value feature, low risk)**
|
||||
Search is central, but right now filtering requires manual field edits. Add facet counts (`issues`, `MRs`, `discussions`, top labels/projects/authors) with one-key apply. This makes search more compelling and actionable without introducing schema changes.
|
||||
|
||||
```diff
|
||||
@@ 5.6 Search
|
||||
-- Layout: Split pane — results list (left) + preview (right)
|
||||
+- Layout: Three-pane on wide terminals — results (left) + preview (center) + facets (right)
|
||||
|
||||
+**Facets panel:**
|
||||
+- Entity type counts (issue/MR/discussion)
|
||||
+- Top labels/projects/authors for current query
|
||||
+- `1/2/3` quick-apply type facet; `l` cycles top label facet
|
||||
|
||||
@@ 8.2 List/Search keybindings
|
||||
+| `1` `2` `3` | Apply facet: Issue / MR / Discussion |
|
||||
+| `l` | Apply next top-label facet |
|
||||
```
|
||||
|
||||
9. **Strengthen text sanitization for terminal edge cases**
|
||||
Current sanitizer is strong, but still misses some control-space edge cases (C1 controls, directional marks beyond the listed bidi set). Add those and test them. This closes spoofing/render confusion gaps with minimal complexity.
|
||||
|
||||
```diff
|
||||
@@ 10.4.1 sanitize_for_terminal()
|
||||
+ // Strip C1 control block (U+0080..U+009F) and additional directional marks
|
||||
+ c if ('\u{0080}'..='\u{009F}').contains(&c) => {}
|
||||
+ '\u{200E}' | '\u{200F}' | '\u{061C}' => {} // LRM, RLM, ALM
|
||||
|
||||
@@ tests
|
||||
+ #[test] fn strips_c1_controls() { ... }
|
||||
+ #[test] fn strips_lrm_rlm_alm() { ... }
|
||||
```
|
||||
|
||||
10. **Add an explicit vertical-slice gate before broad screen expansion**
|
||||
The plan is comprehensive, but risk is still front-loaded on framework + runtime behavior. Insert a strict vertical slice gate (`Dashboard + IssueList + IssueDetail + Sync running`) with perf and stability thresholds before Phase 3 features. This reduces rework if foundational assumptions break.
|
||||
|
||||
```diff
|
||||
@@ 9.2 Phases
|
||||
+section Phase 2.5 — Vertical Slice Gate
|
||||
+Dashboard + IssueList + IssueDetail + Sync (running) integrated :p25a, after p2c, 3d
|
||||
+Gate: p95 nav latency < 75ms on M tier; zero stuck-input-state bugs; cancel p95 < 2s :p25b, after p25a, 1d
|
||||
+Only then proceed to Search/Timeline/Who/Palette expansion.
|
||||
```
|
||||
|
||||
If you want, I can produce a full consolidated `diff` block against the entire PRD text (single patch), but the above is the set I’d prioritize first.
|
||||
7983
plans/tui-prd-v2-frankentui.md
Normal file
7983
plans/tui-prd-v2-frankentui.md
Normal file
File diff suppressed because it is too large
Load Diff
2075
plans/tui-prd.md
Normal file
2075
plans/tui-prd.md
Normal file
File diff suppressed because it is too large
Load Diff
157
plans/work-item-status-graphql.feedback-3.md
Normal file
157
plans/work-item-status-graphql.feedback-3.md
Normal file
@@ -0,0 +1,157 @@
|
||||
**Top Revisions I Recommend**
|
||||
|
||||
1. **Fix auth semantics + a real inconsistency in your test plan**
|
||||
Your ACs require graceful handling for `403`, but the test list says the “403” test returns `401`. That hides the exact behavior you care about and can let permission regressions slip through.
|
||||
|
||||
```diff
|
||||
@@ AC-1: GraphQL Client (Unit)
|
||||
- [ ] HTTP 401 → `LoreError::GitLabAuthFailed`
|
||||
+ [ ] HTTP 401 → `LoreError::GitLabAuthFailed`
|
||||
+ [ ] HTTP 403 → `LoreError::GitLabForbidden`
|
||||
|
||||
@@ AC-3: Status Fetcher (Integration)
|
||||
- [ ] GraphQL 403 → returns `Ok(HashMap::new())` with warning log
|
||||
+ [ ] GraphQL 403 (`GitLabForbidden`) → returns `Ok(HashMap::new())` with warning log
|
||||
|
||||
@@ TDD Plan (RED)
|
||||
- 13. `test_fetch_statuses_403_graceful` — mock returns 401 → `Ok(HashMap::new())`
|
||||
+ 13. `test_fetch_statuses_403_graceful` — mock returns 403 → `Ok(HashMap::new())`
|
||||
```
|
||||
|
||||
2. **Make enrichment atomic and stale-safe**
|
||||
Current plan can leave stale status values forever when a widget disappears or status becomes null. Make writes transactional and clear status fields for fetched scope before upserts.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
+ [ ] Enrichment DB writes are transactional per project (all-or-nothing)
|
||||
+ [ ] Status fields are cleared for fetched issue scope before applying new statuses
|
||||
+ [ ] If enrichment fails mid-project, prior persisted statuses are unchanged (rollback)
|
||||
|
||||
@@ File 6: `src/ingestion/orchestrator.rs`
|
||||
- fn enrich_issue_statuses(...)
|
||||
+ fn enrich_issue_statuses_txn(...)
|
||||
+ // BEGIN TRANSACTION
|
||||
+ // clear status columns for fetched issue scope
|
||||
+ // apply updates
|
||||
+ // COMMIT
|
||||
```
|
||||
|
||||
3. **Add transient retry/backoff (429/5xx/network)**
|
||||
Right now one transient failure loses status enrichment for that sync. Retrying with bounded backoff gives much better reliability at low cost.
|
||||
|
||||
```diff
|
||||
@@ AC-1: GraphQL Client (Unit)
|
||||
+ [ ] Retries 429/502/503/504/network errors with bounded exponential backoff + jitter (max 3 attempts)
|
||||
+ [ ] Honors `Retry-After` on 429 before retrying
|
||||
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
+ [ ] Cancellation signal is checked before each retry sleep and between paginated calls
|
||||
```
|
||||
|
||||
4. **Stop full GraphQL scans when nothing changed**
|
||||
Running full pagination on every sync will dominate runtime on large repos. Trigger enrichment only when issue ingestion reports changes, with a manual override.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
- [ ] Runs on every sync (not gated by `--full`)
|
||||
+ [ ] Runs when issue ingestion changed at least one issue in the project
|
||||
+ [ ] New override flag `--refresh-status` forces enrichment even with zero issue deltas
|
||||
+ [ ] Optional periodic full refresh (e.g. every N syncs) to prevent long-tail drift
|
||||
```
|
||||
|
||||
5. **Do not expose raw token via `client.token()`**
|
||||
Architecturally cleaner and safer: keep token encapsulated and expose a GraphQL-ready client factory from `GitLabClient`.
|
||||
|
||||
```diff
|
||||
@@ File 13: `src/gitlab/client.rs`
|
||||
- pub fn token(&self) -> &str
|
||||
+ pub fn graphql_client(&self) -> crate::gitlab::graphql::GraphqlClient
|
||||
|
||||
@@ File 6: `src/ingestion/orchestrator.rs`
|
||||
- let graphql_client = GraphqlClient::new(&config.gitlab.base_url, client.token());
|
||||
+ let graphql_client = client.graphql_client();
|
||||
```
|
||||
|
||||
6. **Add indexes for new status filters**
|
||||
`--status` on large tables will otherwise full-scan `issues`. Add compound indexes aligned with project-scoped list queries.
|
||||
|
||||
```diff
|
||||
@@ AC-4: Migration 021 (Unit)
|
||||
+ [ ] Adds index `idx_issues_project_status_name(project_id, status_name)`
|
||||
+ [ ] Adds index `idx_issues_project_status_category(project_id, status_category)`
|
||||
|
||||
@@ File 14: `migrations/021_work_item_status.sql`
|
||||
ALTER TABLE issues ADD COLUMN status_name TEXT;
|
||||
ALTER TABLE issues ADD COLUMN status_category TEXT;
|
||||
ALTER TABLE issues ADD COLUMN status_color TEXT;
|
||||
ALTER TABLE issues ADD COLUMN status_icon_name TEXT;
|
||||
+CREATE INDEX IF NOT EXISTS idx_issues_project_status_name
|
||||
+ ON issues(project_id, status_name);
|
||||
+CREATE INDEX IF NOT EXISTS idx_issues_project_status_category
|
||||
+ ON issues(project_id, status_category);
|
||||
```
|
||||
|
||||
7. **Improve filter UX: add category filter + case-insensitive status**
|
||||
Case-sensitive exact name matches are brittle with custom lifecycle names. Category filter is stable and useful for automation.
|
||||
|
||||
```diff
|
||||
@@ AC-9: List Issues Filter (E2E)
|
||||
- [ ] Filter is case-sensitive (matches GitLab's exact status name)
|
||||
+ [ ] `--status` uses case-insensitive exact match by default (`COLLATE NOCASE`)
|
||||
+ [ ] New filter `--status-category` supports `triage|to_do|in_progress|done|canceled`
|
||||
+ [ ] `--status-exact` enables strict case-sensitive behavior when needed
|
||||
```
|
||||
|
||||
8. **Add capability probe/cache to avoid pointless calls**
|
||||
Free tier / old GitLab versions will never return status widget. Cache that capability per project (with TTL) to reduce noise and wasted requests.
|
||||
|
||||
```diff
|
||||
@@ GitLab API Constraints
|
||||
+### Capability Probe
|
||||
+On first sync per project, detect status-widget support and cache result for 24h.
|
||||
+If unsupported, skip enrichment silently (debug log) until TTL expiry.
|
||||
|
||||
@@ AC-3: Status Fetcher (Integration)
|
||||
+ [ ] Unsupported capability state bypasses GraphQL fetch and warning spam
|
||||
```
|
||||
|
||||
9. **Use a nested robot `status` object instead of 4 top-level fields**
|
||||
This is cleaner schema design and scales better as status metadata grows (IDs, lifecycle, timestamps, etc.).
|
||||
|
||||
```diff
|
||||
@@ AC-7: Show Issue Display (Robot)
|
||||
- [ ] JSON includes `status_name`, `status_category`, `status_color`, `status_icon_name` fields
|
||||
- [ ] Fields are `null` (not absent) when status not available
|
||||
+ [ ] JSON includes `status` object:
|
||||
+ `{ "name": "...", "category": "...", "color": "...", "icon_name": "..." }` or `null`
|
||||
|
||||
@@ AC-8: List Issues Display (Robot)
|
||||
- [ ] JSON includes `status_name`, `status_category` fields on each issue
|
||||
+ [ ] JSON includes `status` object (or `null`) on each issue
|
||||
```
|
||||
|
||||
10. **Add one compelling feature: status analytics, not just status display**
|
||||
Right now this is mostly a transport/display enhancement. Make it genuinely useful with “stale in-progress” detection and age-in-status filters.
|
||||
|
||||
```diff
|
||||
@@ Acceptance Criteria
|
||||
+### AC-11: Status Aging & Triage Value (E2E)
|
||||
+- [ ] `lore list issues --status-category in_progress --stale-days 14` filters to stale work
|
||||
+- [ ] Human table shows `Status Age` (days) when status exists
|
||||
+- [ ] Robot output includes `status_age_days` (nullable integer)
|
||||
```
|
||||
|
||||
11. **Harden test plan around failure modes you’ll actually hit**
|
||||
The current tests are good, but miss rollback/staleness/retry behavior that drives real reliability.
|
||||
|
||||
```diff
|
||||
@@ TDD Plan (RED) additions
|
||||
+21. `test_enrich_clears_removed_status`
|
||||
+22. `test_enrich_transaction_rolls_back_on_failure`
|
||||
+23. `test_graphql_retry_429_then_success`
|
||||
+24. `test_graphql_retry_503_then_success`
|
||||
+25. `test_cancel_during_backoff_aborts_cleanly`
|
||||
+26. `test_status_filter_query_uses_project_status_index` (EXPLAIN smoke test)
|
||||
```
|
||||
|
||||
If you want, I can produce a fully revised v3 plan document end-to-end (frontmatter + reordered ACs + updated file list + updated TDD matrix) so it is ready to implement directly.
|
||||
159
plans/work-item-status-graphql.feedback-4.md
Normal file
159
plans/work-item-status-graphql.feedback-4.md
Normal file
@@ -0,0 +1,159 @@
|
||||
Your plan is already strong, but I’d revise it in 10 places to reduce risk at scale and make it materially more useful.
|
||||
|
||||
1. Shared transport + retries for GraphQL (must-have)
|
||||
Reasoning: `REST` already has throttling/retry in `src/gitlab/client.rs`; your proposed GraphQL client would bypass that and can spike rate limits under concurrent project ingest (`src/cli/commands/ingest.rs`). Unifying transport prevents split behavior and cuts production incidents.
|
||||
|
||||
```diff
|
||||
@@ AC-1: GraphQL Client (Unit)
|
||||
- [ ] Network error → `LoreError::Other`
|
||||
+ [ ] GraphQL requests use shared GitLab transport (same timeout, rate limiter, retry policy as REST)
|
||||
+ [ ] Retries 429/502/503/504/network errors (max 3) with exponential backoff + jitter
|
||||
+ [ ] 429 honors `Retry-After` before retrying
|
||||
+ [ ] Exhausted network retries → `LoreError::GitLabNetworkError`
|
||||
|
||||
@@ Decisions
|
||||
- 8. **No retry/backoff in v1** — DEFER.
|
||||
+ 8. **Retry/backoff in v1** — YES (shared REST+GraphQL reliability policy).
|
||||
|
||||
@@ Implementation Detail
|
||||
+ File 15: `src/gitlab/transport.rs` (NEW) — shared HTTP execution and retry/backoff policy.
|
||||
```
|
||||
|
||||
2. Capability cache for unsupported projects (must-have)
|
||||
Reasoning: Free tier / older GitLab will repeatedly emit warning noise every sync and waste calls. Cache support status per project and re-probe on TTL.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
- [ ] On any GraphQL error: logs warning, continues to next project (never fails the sync)
|
||||
+ [ ] Unsupported capability responses (missing endpoint/type/widget) are cached per project
|
||||
+ [ ] While cached unsupported, enrichment is skipped without repeated warning spam
|
||||
+ [ ] Capability cache auto-expires (default 24h) and is re-probed
|
||||
|
||||
@@ Migration Numbering
|
||||
- This feature uses **migration 021**.
|
||||
+ This feature uses **migrations 021-022**.
|
||||
|
||||
@@ Files Changed (Summary)
|
||||
+ `migrations/022_project_capabilities.sql` | NEW — support cache table for project capabilities
|
||||
```
|
||||
|
||||
3. Delta-first enrichment with periodic full reconcile (must-have)
|
||||
Reasoning: Full GraphQL scan every sync is expensive for large projects. You already compute issue deltas in ingestion; use that as fast path and keep a periodic full sweep as safety net.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
- [ ] Runs on every sync (not gated by `--full`)
|
||||
+ [ ] Fast path: skip status enrichment when issue ingestion upserted 0 issues for that project
|
||||
+ [ ] Safety net: run full reconciliation every `status_full_reconcile_hours` (default 24)
|
||||
+ [ ] `--full` always forces reconciliation
|
||||
|
||||
@@ AC-5: Config Toggle (Unit)
|
||||
+ [ ] `SyncConfig` has `status_full_reconcile_hours: u32` (default 24)
|
||||
```
|
||||
|
||||
4. Strongly typed widget parsing via `__typename` (must-have)
|
||||
Reasoning: current “deserialize arbitrary widget JSON into `StatusWidget`” is fragile. Query/type by `__typename` for forward compatibility and fewer silent parse mistakes.
|
||||
|
||||
```diff
|
||||
@@ AC-3: Status Fetcher (Integration)
|
||||
- [ ] Extracts status from `widgets` array by matching `WorkItemWidgetStatus` fragment
|
||||
+ [ ] Query includes `widgets { __typename ... }` and parser matches `__typename == "WorkItemWidgetStatus"`
|
||||
+ [ ] Non-status widgets are ignored deterministically (no heuristic JSON-deserialize attempts)
|
||||
|
||||
@@ GraphQL Query
|
||||
+ widgets {
|
||||
+ __typename
|
||||
+ ... on WorkItemWidgetStatus { ... }
|
||||
+ }
|
||||
```
|
||||
|
||||
5. Set-based transactional DB apply (must-have)
|
||||
Reasoning: row-by-row clear/update loops will be slow on large projects and hold write locks longer. Temp-table + set-based SQL inside one txn is faster and easier to reason about rollback.
|
||||
|
||||
```diff
|
||||
@@ AC-3: Status Fetcher (Integration)
|
||||
- `all_fetched_iids: Vec<i64>`
|
||||
+ `all_fetched_iids: HashSet<i64>`
|
||||
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
- [ ] Before applying updates, NULL out status fields ... (loop per IID)
|
||||
- [ ] UPDATE SQL: `SET status_name=?, ... WHERE project_id=? AND iid=?`
|
||||
+ [ ] Use temp tables and set-based SQL in one transaction:
|
||||
+ [ ] (1) clear stale statuses for fetched IIDs absent from status rows
|
||||
+ [ ] (2) apply status values for fetched IIDs with status
|
||||
+ [ ] One commit per project; rollback leaves prior state intact
|
||||
```
|
||||
|
||||
6. Fix index strategy for `COLLATE NOCASE` + default sorting (must-have)
|
||||
Reasoning: your proposed `(project_id, status_name)` index may not fully help `COLLATE NOCASE` + `ORDER BY updated_at`. Tune index to real query shape in `src/cli/commands/list.rs`.
|
||||
|
||||
```diff
|
||||
@@ AC-4: Migration 021 (Unit)
|
||||
- [ ] Adds compound index `idx_issues_project_status_name(project_id, status_name)` for `--status` filter performance
|
||||
+ [ ] Adds covering NOCASE-aware index:
|
||||
+ [ ] `idx_issues_project_status_name_nocase_updated(project_id, status_name COLLATE NOCASE, updated_at DESC)`
|
||||
+ [ ] Adds category index:
|
||||
+ [ ] `idx_issues_project_status_category_nocase(project_id, status_category COLLATE NOCASE)`
|
||||
```
|
||||
|
||||
7. Add stable/automation-friendly filters now (high-value feature)
|
||||
Reasoning: status names are user-customizable and renameable; category is more stable. Also add `--no-status` for quality checks and migration visibility.
|
||||
|
||||
```diff
|
||||
@@ AC-9: List Issues Filter (E2E)
|
||||
+ [ ] `lore list issues --status-category in_progress` filters by category (case-insensitive)
|
||||
+ [ ] `lore list issues --no-status` returns only issues where `status_name IS NULL`
|
||||
+ [ ] `--status` + `--status-category` combine with AND logic
|
||||
|
||||
@@ File 9: `src/cli/mod.rs`
|
||||
+ Add flags: `--status-category`, `--no-status`
|
||||
|
||||
@@ File 11: `src/cli/autocorrect.rs`
|
||||
+ Register `--status-category` and `--no-status` for `issues`
|
||||
```
|
||||
|
||||
8. Better enrichment observability and failure accounting (must-have ops)
|
||||
Reasoning: only tracking `statuses_enriched` hides skipped/cleared/errors, and auth failures become silent partial data quality issues. Add counters and explicit progress events.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
- [ ] `IngestProjectResult` gains `statuses_enriched: usize` counter
|
||||
- [ ] Progress event: `ProgressEvent::StatusEnrichmentComplete { enriched: usize }`
|
||||
+ [ ] `IngestProjectResult` gains:
|
||||
+ [ ] `statuses_enriched`, `statuses_cleared`, `status_enrichment_skipped`, `status_enrichment_failed`
|
||||
+ [ ] Progress events:
|
||||
+ [ ] `StatusEnrichmentStarted`, `StatusEnrichmentSkipped`, `StatusEnrichmentComplete`, `StatusEnrichmentFailed`
|
||||
+ [ ] End-of-sync summary includes per-project enrichment outcome counts
|
||||
```
|
||||
|
||||
9. Add `status_changed_at` for immediately useful workflow analytics (high-value feature)
|
||||
Reasoning: without change timestamp, you can’t answer “how long has this been in progress?” which is one of the most useful agent/human queries.
|
||||
|
||||
```diff
|
||||
@@ AC-4: Migration 021 (Unit)
|
||||
+ [ ] Adds nullable INTEGER column `status_changed_at` (ms epoch UTC)
|
||||
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
+ [ ] If status_name/category changes, update `status_changed_at = now_ms()`
|
||||
+ [ ] If status is cleared, set `status_changed_at = NULL`
|
||||
|
||||
@@ AC-9: List Issues Filter (E2E)
|
||||
+ [ ] `lore list issues --stale-status-days N` filters by `status_changed_at <= now - N days`
|
||||
```
|
||||
|
||||
10. Expand test matrix for real-world failure/perf paths (must-have)
|
||||
Reasoning: current tests are good, but the highest-risk failures are retry behavior, capability caching, idempotency under repeated runs, and large-project performance.
|
||||
|
||||
```diff
|
||||
@@ TDD Plan — RED Phase
|
||||
+ 26. `test_graphql_retries_429_with_retry_after_then_succeeds`
|
||||
+ 27. `test_graphql_retries_503_then_fails_after_max_attempts`
|
||||
+ 28. `test_capability_cache_skips_unsupported_project_until_ttl_expiry`
|
||||
+ 29. `test_delta_skip_when_no_issue_upserts`
|
||||
+ 30. `test_periodic_full_reconcile_runs_after_threshold`
|
||||
+ 31. `test_set_based_enrichment_scales_10k_issues_without_timeout`
|
||||
+ 32. `test_enrichment_idempotent_across_two_runs`
|
||||
+ 33. `test_status_changed_at_updates_only_on_actual_status_change`
|
||||
```
|
||||
|
||||
If you want, I can now produce a single consolidated revised plan document (full rewritten Markdown) with these changes merged in-place so it’s ready to execute.
|
||||
124
plans/work-item-status-graphql.feedback-5.md
Normal file
124
plans/work-item-status-graphql.feedback-5.md
Normal file
@@ -0,0 +1,124 @@
|
||||
Your plan is already strong and implementation-aware. The best upgrades are mostly about reliability under real-world API instability, large-scale performance, and making the feature more useful for automation.
|
||||
|
||||
1. Promote retry/backoff from deferred to in-scope now.
|
||||
Reason: Right now, transient failures cause silent status gaps until a later sync. Bounded retries with jitter and a time budget dramatically improve successful enrichment without making syncs hang.
|
||||
|
||||
```diff
|
||||
@@ AC-1: GraphQL Client (Unit) @@
|
||||
- [ ] Network error → `LoreError::Other`
|
||||
+ [ ] Transient failures (`429`, `502`, `503`, `504`, timeout, connect reset) retry with exponential backoff + jitter (max 3 attempts)
|
||||
+ [ ] `Retry-After` supports both delta-seconds and HTTP-date formats
|
||||
+ [ ] Per-request retry budget capped (e.g. 120s total) to preserve cancellation responsiveness
|
||||
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration) @@
|
||||
- [ ] On any GraphQL error: logs warning, continues to next project (never fails the sync)
|
||||
+ [ ] On transient GraphQL errors: retry policy applied before warning/skip behavior
|
||||
|
||||
@@ Decisions @@
|
||||
- 8. **No retry/backoff in v1** — DEFER.
|
||||
+ 8. **Retry/backoff in v1** — YES. Required for reliable enrichment under normal GitLab/API turbulence.
|
||||
```
|
||||
|
||||
2. Add a capability cache so unsupported projects stop paying repeated GraphQL cost.
|
||||
Reason: Free tier / older instances will never return status widgets. Re-querying every sync is wasted time and noisy logs.
|
||||
|
||||
```diff
|
||||
@@ Acceptance Criteria @@
|
||||
+ ### AC-11: Capability Probe & Cache (Integration)
|
||||
+ - [ ] Add `project_capabilities` cache with `supports_work_item_status`, `checked_at`, `cooldown_until`
|
||||
+ - [ ] 404/403/known-unsupported responses update capability cache and suppress repeated warnings until TTL expires
|
||||
+ - [ ] Supported projects still enrich every run (subject to normal schedule)
|
||||
|
||||
@@ Future Enhancements (Not in Scope) @@
|
||||
- **Capability probe/cache**: Detect status-widget support per project ... (deferred)
|
||||
+ (moved into scope as AC-11)
|
||||
```
|
||||
|
||||
3. Make enrichment delta-aware with periodic forced reconciliation.
|
||||
Reason: Full pagination every sync is expensive on large projects. You can skip unnecessary status fetches when no issue changes occurred, while still doing periodic safety sweeps.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration) @@
|
||||
- [ ] Runs on every sync (not gated by `--full`)
|
||||
+ [ ] Runs when issue ingestion reports project issue deltas OR reconcile window elapsed
|
||||
+ [ ] New config: `status_reconcile_hours` (default: 24) for periodic full sweep
|
||||
+ [ ] `--refresh-status` forces enrichment regardless of delta/reconcile window
|
||||
```
|
||||
|
||||
4. Replace row-by-row update loops with set-based SQL via temp table.
|
||||
Reason: Current per-IID loops are simple but slow at scale and hold locks longer. Set-based updates are much faster and reduce lock contention.
|
||||
|
||||
```diff
|
||||
@@ File 6: `src/ingestion/orchestrator.rs` (MODIFY) @@
|
||||
- for iid in all_fetched_iids { ... UPDATE issues ... }
|
||||
- for (iid, status) in statuses { ... UPDATE issues ... }
|
||||
+ CREATE TEMP TABLE temp_issue_status_updates(...)
|
||||
+ bulk INSERT temp rows (iid, name, category, color, icon_name)
|
||||
+ single set-based UPDATE for enriched rows
|
||||
+ single set-based NULL-clear for fetched-without-status rows
|
||||
+ commit transaction
|
||||
```
|
||||
|
||||
5. Add strict mode and explicit partial-failure reporting.
|
||||
Reason: “Warn and continue” is good default UX, but automation needs a fail-fast option and machine-readable failure output.
|
||||
|
||||
```diff
|
||||
@@ AC-5: Config Toggle (Unit) @@
|
||||
+ - [ ] `SyncConfig` adds `status_enrichment_strict: bool` (default false)
|
||||
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration) @@
|
||||
- [ ] On any GraphQL error: logs warning, continues to next project (never fails the sync)
|
||||
+ [ ] Default mode: warn + continue
|
||||
+ [ ] Strict mode: status enrichment error fails sync for that run
|
||||
|
||||
@@ AC-6: IngestProjectResult @@
|
||||
+ - [ ] Adds `status_enrichment_error: Option<String>`
|
||||
|
||||
@@ AC-8 / Robot sync envelope @@
|
||||
+ - [ ] Robot output includes `partial_failures` array with per-project enrichment failures
|
||||
```
|
||||
|
||||
6. Fix case-insensitive matching robustness and track freshness.
|
||||
Reason: SQLite `COLLATE NOCASE` is ASCII-centric; custom statuses may be non-ASCII. Also you need visibility into staleness.
|
||||
|
||||
```diff
|
||||
@@ AC-4: Migration 021 (Unit) @@
|
||||
- [ ] Migration adds 4 nullable TEXT columns to `issues`
|
||||
+ [ ] Migration adds 6 columns:
|
||||
+ `status_name`, `status_category`, `status_color`, `status_icon_name`,
|
||||
+ `status_name_fold`, `status_synced_at`
|
||||
- [ ] Adds compound index `idx_issues_project_status_name(project_id, status_name)`
|
||||
+ [ ] Adds compound index `idx_issues_project_status_name_fold(project_id, status_name_fold)`
|
||||
|
||||
@@ AC-9: List Issues Filter (E2E) @@
|
||||
- [ ] Filter uses case-insensitive matching (`COLLATE NOCASE`)
|
||||
+ [ ] Filter uses `status_name_fold` (Unicode-safe fold normalization done at write time)
|
||||
```
|
||||
|
||||
7. Expand filtering to category and missing-status workflows.
|
||||
Reason: Name filters are useful, but automation is better on semantic categories and “missing data” detection.
|
||||
|
||||
```diff
|
||||
@@ AC-9: List Issues Filter (E2E) @@
|
||||
+ - [ ] `--status-category in_progress` filters by `status_category` (case-insensitive)
|
||||
+ - [ ] `--no-status` returns only issues where `status_name IS NULL`
|
||||
+ - [ ] `--status` and `--status-category` can be combined with AND logic
|
||||
```
|
||||
|
||||
8. Change robot payload from flat status fields to a nested `status` object.
|
||||
Reason: Better schema evolution and less top-level field sprawl as you add metadata (`synced_at`, future lifecycle fields).
|
||||
|
||||
```diff
|
||||
@@ AC-7: Show Issue Display (E2E) @@
|
||||
- [ ] JSON includes `status_name`, `status_category`, `status_color`, `status_icon_name` fields
|
||||
- [ ] Fields are `null` (not absent) when status not available
|
||||
+ [ ] JSON includes `status` object:
|
||||
+ `{ "name", "category", "color", "icon_name", "synced_at" }`
|
||||
+ [ ] `status: null` when not available
|
||||
|
||||
@@ AC-8: List Issues Display (E2E) @@
|
||||
- [ ] `--fields` supports: `status_name`, `status_category`, `status_color`, `status_icon_name`
|
||||
+ [ ] `--fields` supports: `status.name,status.category,status.color,status.icon_name,status.synced_at`
|
||||
```
|
||||
|
||||
If you want, I can produce a fully rewritten “Iteration 5” plan document with these changes integrated end-to-end (ACs, files, migrations, TDD batches, and updated decisions/future-scope).
|
||||
130
plans/work-item-status-graphql.feedback-6.md
Normal file
130
plans/work-item-status-graphql.feedback-6.md
Normal file
@@ -0,0 +1,130 @@
|
||||
Your iteration-5 plan is strong. The biggest remaining gaps are outcome ambiguity, cancellation safety, and long-term status identity. These are the revisions I’d make.
|
||||
|
||||
1. **Make enrichment outcomes explicit (not “empty success”)**
|
||||
Analysis:
|
||||
Right now `404/403 -> Ok(empty)` is operationally ambiguous: “project has no statuses” vs “feature unavailable/auth issue.” Agents and dashboards need that distinction to make correct decisions.
|
||||
This improves reliability and observability without making sync fail-hard.
|
||||
|
||||
```diff
|
||||
@@ AC-3: Status Fetcher (Integration)
|
||||
-- [ ] `fetch_issue_statuses()` returns `FetchStatusResult` containing:
|
||||
+- [ ] `fetch_issue_statuses()` returns `FetchStatusOutcome`:
|
||||
+ - `Fetched(FetchStatusResult)`
|
||||
+ - `Unsupported { reason: UnsupportedReason }`
|
||||
+ - `CancelledPartial(FetchStatusResult)`
|
||||
@@
|
||||
-- [ ] GraphQL 404 → returns `Ok(FetchStatusResult)` with empty collections + warning log
|
||||
-- [ ] GraphQL 403 (`GitLabAuthFailed`) → returns `Ok(FetchStatusResult)` with empty collections + warning log
|
||||
+- [ ] GraphQL 404 → `Unsupported { reason: GraphqlEndpointMissing }` + warning log
|
||||
+- [ ] GraphQL 403 (`GitLabAuthFailed`) → `Unsupported { reason: AuthForbidden }` + warning log
|
||||
|
||||
@@ AC-10: Robot Sync Envelope (E2E)
|
||||
-- [ ] `status_enrichment` object: `{ "enriched": N, "cleared": N, "error": null | "message" }`
|
||||
+- [ ] `status_enrichment` object: `{ "mode": "fetched|unsupported|cancelled_partial", "reason": null|"...", "enriched": N, "cleared": N, "error": null|"message" }`
|
||||
```
|
||||
|
||||
2. **Add cancellation and pagination loop safety**
|
||||
Analysis:
|
||||
Large projects can run long. Current flow checks cancellation only before enrichment starts; pagination and per-row update loops can ignore cancellation for too long. Also, GraphQL cursor bugs can create infinite loops (`hasNextPage=true` with unchanged cursor).
|
||||
This is a robustness must-have.
|
||||
|
||||
```diff
|
||||
@@ AC-3: Status Fetcher (Integration)
|
||||
+ [ ] `fetch_issue_statuses()` accepts cancellation signal and checks it between page requests
|
||||
+ [ ] Pagination guard: if `hasNextPage=true` but `endCursor` is `None` or unchanged, abort loop with warning and return partial outcome
|
||||
+ [ ] Emits `pages_fetched` count for diagnostics
|
||||
|
||||
@@ File 1: `src/gitlab/graphql.rs`
|
||||
-- pub async fn fetch_issue_statuses(client: &GraphqlClient, project_path: &str) -> Result<FetchStatusResult>
|
||||
+- pub async fn fetch_issue_statuses(client: &GraphqlClient, project_path: &str, signal: &CancellationSignal) -> Result<FetchStatusOutcome>
|
||||
```
|
||||
|
||||
3. **Persist stable `status_id` in addition to name**
|
||||
Analysis:
|
||||
`status_name` is display-oriented and mutable (rename/custom lifecycle changes). A stable status identifier is critical for durable automations, analytics, and future migrations.
|
||||
This is a schema decision that is cheap now and expensive later if skipped.
|
||||
|
||||
```diff
|
||||
@@ AC-2: Status Types (Unit)
|
||||
-- [ ] `WorkItemStatus` struct has `name`, `category`, `color`, `icon_name`
|
||||
+- [ ] `WorkItemStatus` struct has `id: String`, `name`, `category`, `color`, `icon_name`
|
||||
|
||||
@@ AC-4: Migration 021 (Unit)
|
||||
-- [ ] Migration adds 5 nullable columns to `issues`: `status_name`, `status_category`, `status_color`, `status_icon_name`, `status_synced_at`
|
||||
+- [ ] Migration adds 6 nullable columns to `issues`: `status_id`, `status_name`, `status_category`, `status_color`, `status_icon_name`, `status_synced_at`
|
||||
+ [ ] Adds index `idx_issues_project_status_id(project_id, status_id)` for stable-machine filters
|
||||
|
||||
@@ GraphQL query
|
||||
- status { name category color iconName }
|
||||
+ status { id name category color iconName }
|
||||
|
||||
@@ AC-7 / AC-8 Robot
|
||||
+ [ ] JSON includes `status_id` (null when unavailable)
|
||||
```
|
||||
|
||||
4. **Handle GraphQL partial-data responses correctly**
|
||||
Analysis:
|
||||
GraphQL can return both `data` and `errors` in the same response. Current plan treats any `errors` as hard failure, which can discard valid data and reduce reliability.
|
||||
Use partial-data semantics: keep data, log/report warnings.
|
||||
|
||||
```diff
|
||||
@@ AC-1: GraphQL Client (Unit)
|
||||
-- [ ] Error response: if top-level `errors` array is non-empty, returns `LoreError` with first error message
|
||||
+- [ ] If `errors` non-empty and `data` missing: return `LoreError` with first error message
|
||||
+- [ ] If `errors` non-empty and `data` present: return `data` + warning metadata (do not fail the whole fetch)
|
||||
|
||||
@@ TDD Plan (RED)
|
||||
+ 33. `test_graphql_partial_data_with_errors_returns_data_and_warning`
|
||||
```
|
||||
|
||||
5. **Extract status enrichment from orchestrator into dedicated module**
|
||||
Analysis:
|
||||
`orchestrator.rs` already has many phases. Putting status transport/parsing/transaction policy directly there increases coupling and test friction.
|
||||
A dedicated module improves architecture clarity and makes future enhancements safer.
|
||||
|
||||
```diff
|
||||
@@ Implementation Detail
|
||||
+- File 15: `src/ingestion/enrichment/status.rs` (NEW)
|
||||
+ - `run_status_enrichment(...)`
|
||||
+ - `enrich_issue_statuses_txn(...)`
|
||||
+ - outcome mapping + telemetry
|
||||
|
||||
@@ File 6: `src/ingestion/orchestrator.rs`
|
||||
-- Inline Phase 1.5 logic + helper function
|
||||
+- Delegates to `enrichment::status::run_status_enrichment(...)` and records returned stats
|
||||
```
|
||||
|
||||
6. **Add status/state consistency checks**
|
||||
Analysis:
|
||||
GitLab states status categories and issue state should synchronize, but ingestion drift or API edge cases can violate this. Detecting mismatch is high-signal for data integrity issues.
|
||||
This is compelling for agents because it catches “looks correct but isn’t” problems.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
+ [ ] Enrichment computes `status_state_mismatches` count:
|
||||
+ - `DONE|CANCELED` with `state=open` or `TO_DO|IN_PROGRESS|TRIAGE` with `state=closed`
|
||||
+ [ ] Logs warning summary when mismatches > 0
|
||||
|
||||
@@ AC-10: Robot Sync Envelope (E2E)
|
||||
+ [ ] `status_enrichment` includes `state_mismatches: N`
|
||||
```
|
||||
|
||||
7. **Add explicit performance envelope acceptance criterion**
|
||||
Analysis:
|
||||
Plan claims large-project handling, but no hard validation target is defined. Add a bounded, reproducible performance criterion to prevent regressions.
|
||||
This is especially important with pagination + per-row writes.
|
||||
|
||||
```diff
|
||||
@@ Acceptance Criteria
|
||||
+ ### AC-12: Performance Envelope (Integration)
|
||||
+ - [ ] 10k-issue fixture completes status fetch + apply within defined budget on CI baseline machine
|
||||
+ - [ ] Memory usage remains O(page_size), not O(total_issues)
|
||||
+ - [ ] Cancellation during large sync exits within a bounded latency target
|
||||
|
||||
@@ TDD Plan (RED)
|
||||
+ 34. `test_enrichment_large_project_budget`
|
||||
+ 35. `test_fetch_statuses_memory_bound_by_page`
|
||||
+ 36. `test_cancellation_latency_during_pagination`
|
||||
```
|
||||
|
||||
If you want, I can next produce a single consolidated “iteration 6” plan draft with these diffs fully merged so it’s ready to execute.
|
||||
118
plans/work-item-status-graphql.feedback-7.md
Normal file
118
plans/work-item-status-graphql.feedback-7.md
Normal file
@@ -0,0 +1,118 @@
|
||||
**Highest-Impact Revisions (new, not in your rejected list)**
|
||||
|
||||
1. **Critical: Preserve GraphQL partial-error metadata end-to-end (don’t just log it)**
|
||||
Rationale: Right now partial GraphQL errors are warning-only. Agents get no machine-readable signal that status data may be incomplete, which can silently corrupt downstream automation decisions. Exposing partial-error metadata in `FetchStatusResult` and robot sync output makes reliability observable and actionable.
|
||||
|
||||
```diff
|
||||
@@ AC-1: GraphQL Client (Unit)
|
||||
- [ ] Partial-data response: if `errors` array is non-empty BUT `data` field is present and non-null, returns `data` and logs warning with first error message
|
||||
+ [ ] Partial-data response: if `errors` array is non-empty BUT `data` field is present and non-null, returns `data` and warning metadata (`had_errors=true`, `first_error_message`)
|
||||
+ [ ] `GraphqlClient::query()` returns `GraphqlQueryResult { data, had_errors, first_error_message }`
|
||||
|
||||
@@ AC-3: Status Fetcher (Integration)
|
||||
+ [ ] `FetchStatusResult` includes `partial_error_count: usize` and `first_partial_error: Option<String>`
|
||||
+ [ ] Partial GraphQL errors increment `partial_error_count` and are surfaced to orchestrator result
|
||||
|
||||
@@ AC-10: Robot Sync Envelope (E2E)
|
||||
- { "mode": "...", "reason": ..., "enriched": N, "cleared": N, "error": ... }
|
||||
+ { "mode": "...", "reason": ..., "enriched": N, "cleared": N, "error": ..., "partial_errors": N, "first_partial_error": null|"..." }
|
||||
|
||||
@@ File 1: src/gitlab/graphql.rs
|
||||
- pub async fn query(...) -> Result<serde_json::Value>
|
||||
+ pub async fn query(...) -> Result<GraphqlQueryResult>
|
||||
+ pub struct GraphqlQueryResult { pub data: serde_json::Value, pub had_errors: bool, pub first_error_message: Option<String> }
|
||||
```
|
||||
|
||||
2. **High: Add adaptive page-size fallback for GraphQL complexity/timeout failures**
|
||||
Rationale: Fixed `first: 100` is brittle on self-hosted instances with stricter complexity/time limits. Adaptive page size (100→50→25→10) improves success rate without retries/backoff and avoids failing an entire project due to one tunable server constraint.
|
||||
|
||||
```diff
|
||||
@@ Query Path
|
||||
-query($projectPath: ID!, $after: String) { ... workItems(types: [ISSUE], first: 100, after: $after) ... }
|
||||
+query($projectPath: ID!, $after: String, $first: Int!) { ... workItems(types: [ISSUE], first: $first, after: $after) ... }
|
||||
|
||||
@@ AC-3: Status Fetcher (Integration)
|
||||
+ [ ] Starts with `first=100`; on GraphQL complexity/timeout errors, retries same cursor with smaller page size (50, 25, 10)
|
||||
+ [ ] If smallest page size still fails, returns error as today
|
||||
+ [ ] Emits warning including page size downgrade event
|
||||
|
||||
@@ TDD Plan (RED)
|
||||
+ 36. `test_fetch_statuses_complexity_error_reduces_page_size`
|
||||
+ 37. `test_fetch_statuses_timeout_error_reduces_page_size`
|
||||
```
|
||||
|
||||
3. **High: Make project path lookup failure non-fatal for the sync**
|
||||
Rationale: Enrichment is optional. If `projects.path_with_namespace` lookup fails for any reason, sync should continue with a structured enrichment error instead of risking full project pipeline failure.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
+ [ ] If project path lookup fails/missing, status enrichment is skipped for that project, warning logged, and sync continues
|
||||
+ [ ] `status_enrichment_error` captures `"project_path_missing"` (or DB error text)
|
||||
|
||||
@@ File 6: src/ingestion/orchestrator.rs
|
||||
- let project_path: String = conn.query_row(...)?;
|
||||
+ let project_path = conn.query_row(...).optional()?;
|
||||
+ if project_path.is_none() {
|
||||
+ result.status_enrichment_error = Some("project_path_missing".to_string());
|
||||
+ result.status_enrichment_mode = "fetched".to_string(); // attempted but unavailable locally
|
||||
+ emit(ProgressEvent::StatusEnrichmentComplete { enriched: 0, cleared: 0 });
|
||||
+ // continue to discussion sync
|
||||
+ }
|
||||
```
|
||||
|
||||
4. **Medium: Upgrade `--status` from single-value to repeatable multi-value filter**
|
||||
Rationale: Practical usage often needs “active buckets” (`To do` OR `In progress`). Repeatable `--status` with OR semantics dramatically improves usefulness without adding new conceptual surface area.
|
||||
|
||||
```diff
|
||||
@@ AC-9: List Issues Filter (E2E)
|
||||
- [ ] `lore list issues --status "In progress"` → only issues where `status_name = 'In progress'`
|
||||
+ [ ] `lore list issues --status "In progress"` → unchanged single-value behavior
|
||||
+ [ ] Repeatable flags supported: `--status "In progress" --status "To do"` (OR semantics across status values)
|
||||
+ [ ] Repeated `--status` remains AND-composed with other filters
|
||||
|
||||
@@ File 9: src/cli/mod.rs
|
||||
- pub status: Option<String>,
|
||||
+ pub status: Vec<String>, // repeatable flag
|
||||
|
||||
@@ File 8: src/cli/commands/list.rs
|
||||
- if let Some(status) = filters.status { where_clauses.push("i.status_name = ? COLLATE NOCASE"); ... }
|
||||
+ if !filters.statuses.is_empty() { /* dynamic OR/IN clause with case-insensitive matching */ }
|
||||
```
|
||||
|
||||
5. **Medium: Add coverage telemetry (`seen`, `with_status`, `without_status`)**
|
||||
Rationale: `enriched`/`cleared` alone is not enough to judge enrichment health. Coverage counters make it obvious whether a project truly has no statuses, is unsupported, or has unexpectedly low status population.
|
||||
|
||||
```diff
|
||||
@@ AC-6: Enrichment in Orchestrator (Integration)
|
||||
+ [ ] `IngestProjectResult` gains `statuses_seen: usize` and `statuses_without_widget: usize`
|
||||
+ [ ] Enrichment log includes `seen`, `enriched`, `cleared`, `without_widget`
|
||||
|
||||
@@ AC-10: Robot Sync Envelope (E2E)
|
||||
- status_enrichment: { mode, reason, enriched, cleared, error }
|
||||
+ status_enrichment: { mode, reason, seen, enriched, cleared, without_widget, error, partial_errors }
|
||||
|
||||
@@ File 6: src/ingestion/orchestrator.rs
|
||||
+ result.statuses_seen = fetch_result.all_fetched_iids.len();
|
||||
+ result.statuses_without_widget = result.statuses_seen.saturating_sub(result.statuses_enriched);
|
||||
```
|
||||
|
||||
6. **Medium: Centralize color parsing/render decisions (single helper used by show/list)**
|
||||
Rationale: Color parsing is duplicated in `show.rs` and `list.rs`, which invites drift and inconsistent behavior. One shared helper gives consistent fallback behavior and simpler tests.
|
||||
|
||||
```diff
|
||||
@@ File 7: src/cli/commands/show.rs
|
||||
- fn style_with_hex(...) { ...hex parse logic... }
|
||||
+ use crate::cli::commands::color::style_with_hex;
|
||||
|
||||
@@ File 8: src/cli/commands/list.rs
|
||||
- fn colored_cell_hex(...) { ...hex parse logic... }
|
||||
+ use crate::cli::commands::color::colored_cell_hex;
|
||||
|
||||
@@ Files Changed (Summary)
|
||||
+ `src/cli/commands/color.rs` (NEW) — shared hex parsing + styling helpers
|
||||
- duplicated hex parsing blocks removed from show/list
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
If you want, I can produce a **single consolidated patch-style diff of the plan document itself** (all section edits merged, ready to paste as iteration 7).
|
||||
1627
plans/work-item-status-graphql.md
Normal file
1627
plans/work-item-status-graphql.md
Normal file
File diff suppressed because it is too large
Load Diff
2036
plans/work-item-status-graphql.tdd-appendix.md
Normal file
2036
plans/work-item-status-graphql.tdd-appendix.md
Normal file
File diff suppressed because it is too large
Load Diff
@@ -61,6 +61,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
||||
"--assignee",
|
||||
"--label",
|
||||
"--milestone",
|
||||
"--status",
|
||||
"--since",
|
||||
"--due-before",
|
||||
"--has-due",
|
||||
@@ -134,6 +135,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
||||
"--since",
|
||||
"--updated-since",
|
||||
"--limit",
|
||||
"--fields",
|
||||
"--explain",
|
||||
"--no-explain",
|
||||
"--fts-mode",
|
||||
@@ -162,6 +164,7 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
||||
"--depth",
|
||||
"--expand-mentions",
|
||||
"--limit",
|
||||
"--fields",
|
||||
"--max-seeds",
|
||||
"--max-entities",
|
||||
"--max-evidence",
|
||||
@@ -177,8 +180,12 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
||||
"--since",
|
||||
"--project",
|
||||
"--limit",
|
||||
"--fields",
|
||||
"--detail",
|
||||
"--no-detail",
|
||||
],
|
||||
),
|
||||
("drift", &["--threshold", "--project"]),
|
||||
(
|
||||
"init",
|
||||
&[
|
||||
@@ -187,10 +194,12 @@ const COMMAND_FLAGS: &[(&str, &[&str])] = &[
|
||||
"--gitlab-url",
|
||||
"--token-env-var",
|
||||
"--projects",
|
||||
"--default-project",
|
||||
],
|
||||
),
|
||||
("generate-docs", &["--full", "--project"]),
|
||||
("completions", &[]),
|
||||
("robot-docs", &["--brief"]),
|
||||
(
|
||||
"list",
|
||||
&[
|
||||
|
||||
@@ -179,9 +179,15 @@ fn count_notes(conn: &Connection, type_filter: Option<&str>) -> Result<CountResu
|
||||
}
|
||||
|
||||
fn format_number(n: i64) -> String {
|
||||
let s = n.to_string();
|
||||
let (prefix, abs) = if n < 0 {
|
||||
("-", n.unsigned_abs())
|
||||
} else {
|
||||
("", n.unsigned_abs())
|
||||
};
|
||||
|
||||
let s = abs.to_string();
|
||||
let chars: Vec<char> = s.chars().collect();
|
||||
let mut result = String::new();
|
||||
let mut result = String::from(prefix);
|
||||
|
||||
for (i, c) in chars.iter().enumerate() {
|
||||
if i > 0 && (chars.len() - i).is_multiple_of(3) {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user