@gi4nks/ph 1.4.0 → 1.6.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (34) hide show
  1. package/CHANGELOG.md +7 -0
  2. package/README.md +147 -58
  3. package/dist/cli.js +2292 -1021
  4. package/dist/cli.js.map +1 -1
  5. package/docs/evals/context-retrieval-golden.json +26 -0
  6. package/docs/evals/context-retrieval.md +27 -0
  7. package/docs/hooks.md +13 -7
  8. package/docs/ph-analysis.html +145 -0
  9. package/docs/ph-manual.md +352 -231
  10. package/docs/specs/SPEC-001-architecture-cli.md +82 -0
  11. package/docs/specs/SPEC-002-database-layer.md +105 -0
  12. package/docs/specs/SPEC-003-capture-modes.md +110 -0
  13. package/docs/specs/SPEC-004-filter-metadata.md +82 -0
  14. package/docs/specs/SPEC-005-search.md +102 -0
  15. package/docs/specs/SPEC-006-analysis-pipeline.md +98 -0
  16. package/docs/specs/SPEC-007-memory-summaries.md +96 -0
  17. package/docs/specs/SPEC-008-embeddings.md +90 -0
  18. package/docs/specs/SPEC-009-retention-archive.md +91 -0
  19. package/docs/specs/SPEC-010-remote-sync.md +106 -0
  20. package/docs/specs/SPEC-011-mcp-server.md +94 -0
  21. package/docs/specs/SPEC-012-tui-browser.md +98 -0
  22. package/docs/specs/SPEC-013-importers.md +109 -0
  23. package/docs/specs/SPEC-014-analytics-commands.md +109 -0
  24. package/docs/specs/SPEC-015-config-env.md +85 -0
  25. package/docs/specs/SPEC-INDEX.md +35 -0
  26. package/docs/specs/SPEC-ISSUES.md +122 -0
  27. package/docs/specs/_TEMPLATE.md +65 -0
  28. package/docs/superpowers/plans/2026-06-12-ph-evolution-phase1.md +466 -0
  29. package/docs/superpowers/plans/2026-06-12-ph-evolution-phase2.md +351 -0
  30. package/docs/superpowers/specs/2026-06-12-ph-evolution-design.md +173 -0
  31. package/hooks/codex/README.md +54 -0
  32. package/hooks/codex/ph-hook.sh +14 -0
  33. package/hooks/opencode/ph-plugin.ts +60 -57
  34. package/package.json +13 -12
@@ -0,0 +1,82 @@
1
+ # SPEC-001: Architecture & CLI Dispatch
2
+
3
+ - **ID**: SPEC-001
4
+ - **Cluster**: Core
5
+ - **Status**: Implemented (baseline)
6
+ - **Date**: 2026-08-08
7
+ - **Source**: reverse-engineered from code (files listed in §8)
8
+
9
+ ## 1. Purpose
10
+ `ph` is a transparent observability layer for AI CLI tools: every prompt+response
11
+ is captured to a local SQLite DB (SPEC-002), analyzed (SPEC-006), and made
12
+ browsable via TUI (SPEC-012), CLI commands, MCP (SPEC-011) and HTTP (SPEC-010).
13
+ This spec covers the entry point and the command dispatch architecture.
14
+
15
+ ## 2. Scope
16
+ - **In scope**: cli.ts main(), command resolution, wrapper-mode fallback, config
17
+ loading order, TUI bootstrap.
18
+ - **Out of scope**: individual commands (their own specs), the TUI internals.
19
+ - **Entry points**: `ph` (no args → TUI), `ph <command>`, `ph <tool> "prompt"` (wrapper).
20
+
21
+ ## 3. Data Model
22
+ - `argv` split into `command` (first non-flag arg) + `cmdArgs` + `preArgs`
23
+ (ph-specific flags before the command, e.g. `--ph-tag`).
24
+ - `PhConfig` (src/config/index.ts:5) loaded before dispatch; `geminiApiKey`
25
+ is copied to `process.env.GEMINI_API_KEY` (cli.ts:153-155).
26
+ - DB path resolution: `PH_DB` env > `cfg.dbPath` > `defaultPath()`
27
+ (cli.ts:157).
28
+
29
+ ## 4. Flows
30
+
31
+ ### 4.1 Dispatch (src/cli.ts:148-441)
32
+ 1. Load config; set env; resolve dbPath.
33
+ 2. No args + TTY → render BrowseApp in alt-screen, wait for exit, optional
34
+ rerun (cli.ts:159-186).
35
+ 3. Find first non-flag arg; flags `--ph-tag`/`--ph-role` consume their value
36
+ (cli.ts:190-203).
37
+ 4. Switch on the command name: ~25 cases (search, context, last, sessions,
38
+ stats, cluster, analyze*, cleanup*, star, export, import, analyze, mcp,
39
+ server, remote, log, embed-all, vacuum, config, _bg-analyze, browse, chat,
40
+ timeline, ollama-models, help).
41
+ 5. Unknown command → wrapper mode: `cmdWrap(dbPath, command, [...preArgs,
42
+ ...cmdArgs], cfg)` (cli.ts:436-439).
43
+
44
+ ### 4.2 Wrapper mode (SPEC-003)
45
+ Unknown first arg = a real binary (`claude`, `gemini`, …): run it and capture.
46
+
47
+ ## 5. Invariants & Business rules
48
+ - ESM only, `.js` import extensions (package.json `"type": "module"`).
49
+ - Manual arg parsing (`parseFlags`, src/commands/_utils.ts:1) — no commander.
50
+ - Every command handler owns its `PhDB` instance and closes it
51
+ (`new PhDB(...)` … `db.close()` per case).
52
+ - `main()` catches everything: `ph: <message>` on stderr, exit 1 (cli.ts:443-446).
53
+
54
+ ## 6. UI / UX surface
55
+ USAGE text is a single ~200-line template literal (cli.ts:38-140); `ph --help`,
56
+ `-h`, `help` and non-TTY no-arg all print it.
57
+
58
+ ## 7. Acceptance criteria (Given/When/Then)
59
+ - **G1**: Given `ph search foo`, When main() runs, Then the `search` case opens a
60
+ PhDB, calls `cmdSearch` and closes the DB (cli.ts:250-253).
61
+ - **G2**: Given `ph claude "hello"` (unknown command), When main() runs, Then
62
+ `cmdWrap` receives tool=`claude` and args `["hello"]` (cli.ts:436-439).
63
+ - **G3**: Given `ph` with no args on a non-TTY stdin, When main() runs, Then the
64
+ USAGE text is printed and the process exits 0 (cli.ts:183-186).
65
+ - **G4**: Given `--ph-tag auth claude "x"`, When parsing, Then `auth` is consumed
66
+ as the tag value and `claude` is the command (cli.ts:192-194).
67
+
68
+ ## 8. Key implementation map
69
+ | Concern | File(s) |
70
+ |---|---|
71
+ | Entry + dispatch + USAGE | src/cli.ts:38-441 |
72
+ | Config load/save | src/config/index.ts:26-40 |
73
+ | Flag parsing | src/commands/_utils.ts:1-30 |
74
+ | Shared TUI bootstrap | src/cli.ts (`openBrowser`, `runRerun`) |
75
+
76
+ ## 9. Open questions / discrepancies
77
+ - `--ph-debug` is listed in USAGE but only consumed by wrapper paths (inline/pty
78
+ debug logs); no validation if used with non-wrapper commands.
79
+
80
+ ## 10. Related
81
+ - All other specs (commands dispatch into each), SPEC-012 (BrowseApp),
82
+ SPEC-003 (wrapper mode). CLI dispatch tests remain part of SPEC-ISSUES-007.
@@ -0,0 +1,105 @@
1
+ # SPEC-002: Database Layer (PhDB)
2
+
3
+ - **ID**: SPEC-002
4
+ - **Cluster**: DB
5
+ - **Status**: Implemented (baseline)
6
+ - **Date**: 2026-08-08
7
+ - **Source**: reverse-engineered from code (files listed in §8)
8
+
9
+ ## 1. Purpose
10
+ All persistence for ph: prompts (raw history), FTS5 full-text index, sqlite-vec
11
+ embeddings, memories, project summaries and the archive — behind a single
12
+ `PhDB` class (src/db/index.ts, ~800 lines, ~50 public methods). WAL mode, best
13
+ effort migrations, zero ORM.
14
+
15
+ ## 2. Scope
16
+ - **In scope**: schema, migrations, insert/update/delete/search primitives,
17
+ memories + summaries + archive APIs, stats.
18
+ - **Out of scope**: FTS query semantics (SPEC-005), semantic search (SPEC-008),
19
+ retention rules (SPEC-009), sync dedup (SPEC-010).
20
+ - **Entry points**: `new PhDB(dbPath)`, `defaultPath()` (src/db/index.ts).
21
+
22
+ ## 3. Data Model
23
+ - `prompts` (src/db/index.ts:56-66): id, timestamp, tool, prompt, args, workdir,
24
+ hostname, exit_code, metadata (JSON). NOTE: `response` column is added by ALTER
25
+ (SPEC-ISSUES-001).
26
+ - `prompts_fts` FTS5 external-content (src/db/index.ts:68-73) with AFTER INSERT /
27
+ AFTER DELETE triggers (75-82).
28
+ - `embeddings` (legacy BLOB, 87-91) + `vec_embeddings` vec0 float[768] (100-104).
29
+ - `memories` (106-121) append-only per project; `project_summaries` (PhDB.PROJECT_SUMMARIES_SCHEMA, :123);
30
+ `prompts_archive` (PhDB.ARCHIVE_SCHEMA, :124).
31
+ - `PromptEntry` / `PromptMetadata` / `MemoryEntry` / `ProjectSummary` types
32
+ (src/types.ts:1-52).
33
+
34
+ ## 4. Flows
35
+
36
+ ### 4.1 Open + migrate (src/db/index.ts:47-196)
37
+ 1. `new Database(dbPath)`; `PRAGMA journal_mode = WAL`; `sqliteVec.load(db)`.
38
+ 2. CREATE TABLE IF NOT EXISTS for all tables + triggers + indexes.
39
+ 3. Migrations: `$schema_version` backfill (127-136), legacy metadata column
40
+ (139-141), `response` column (143-146), BLOB→vec0 embedding migration
41
+ (148-180).
42
+
43
+ ### 4.2 Insert (src/db/index.ts:198-218)
44
+ `insert(entry)` defaults `response ?? ''` and `metadata || '{}'`, INSERTs, FTS
45
+ trigger keeps the index in sync. Returns lastInsertRowid.
46
+
47
+ ### 4.3 Read paths
48
+ - `search(opts)` (238) — see SPEC-005.
49
+ - `getById` (232), `getPromptsSince` (393), `getAllPrompts` (387),
50
+ `getPromptCount`/`getPromptCountSince` (399-410), `getStats` (410+).
51
+ - `getPromptBySyncHash` (403-407) — used by sync dedup (SPEC-010).
52
+ - Memories: `insertMemory` (535), `upsertProjectMemory` (612),
53
+ `upsertProjectSummary` (643), `getProjectSummary` (690) — SPEC-007.
54
+ - Archive: `archivePrompts` (464), `searchArchive` (496), `purgeArchive` (506),
55
+ `getArchiveStats` (511) — SPEC-009.
56
+
57
+ ## 5. Invariants & Business rules
58
+ - WAL mode always on; sqlite-vec loaded on every open.
59
+ - FTS index maintained by triggers (never manually rebuilt except migration).
60
+ - `metadata` is always valid JSON (`'{}'` fallback).
61
+ - All counts/read queries are typed casts on prepared statements — no ORM.
62
+
63
+ ## 6. UI / UX surface
64
+ None — library layer consumed by commands, TUI, MCP and HTTP server.
65
+
66
+ ## 7. Acceptance criteria (Given/When/Then)
67
+ - **G1**: Given a fresh temp DB, When `insert()` runs with a prompt, Then the
68
+ row is retrievable via `getById` and count is 1 (src/db/index.ts:198-232).
69
+ - **G2**: Given an insert without `response`, When read back, Then `response`
70
+ defaults to `''` (src/db/index.ts:199-201).
71
+ - **G3**: Given two prompts with the same `tool|prompt|response`, When
72
+ `getPromptBySyncHash` runs, Then only the first match is returned
73
+ (src/db/index.ts:403-407).
74
+ - **G4**: Given a delete, When `getById` runs, Then the entry is gone and the
75
+ FTS trigger removed it (src/db/index.ts:79-82, 427).
76
+ - **G5**: Given `getStats()`, When run, Then total/totalMemories/byTool are
77
+ consistent with the tables (src/db/index.ts:410+).
78
+ - **G6**: Given a fresh DB, When opened, Then `vec_embeddings` exists as a vec0
79
+ table with float[768] (src/db/index.ts:95-104).
80
+
81
+ ## 8. Key implementation map
82
+ | Concern | File(s) |
83
+ |---|---|
84
+ | Schema + migrations | src/db/index.ts:47-196 |
85
+ | CRUD + search + sync primitives | src/db/index.ts:198-531 |
86
+ | Memories + summaries | src/db/index.ts:535-720 |
87
+ | Archive | src/db/index.ts:464-518 |
88
+ | Semantic vector index | src/db/semantic-index.ts |
89
+ | Types | src/types.ts:1-52 |
90
+
91
+ ## 9. Open questions / discrepancies
92
+ - **Architecture decision (2026-10-09)**: keep `PhDB` as the single SQLite adapter
93
+ for now. Context consumers depend on the focused `ProjectContextStore` interface
94
+ (`src/context/index.ts`). Vector encoding, compatibility storage, and KNN
95
+ ranking now live in `src/db/semantic-index.ts`, with `PhDB` retaining its public
96
+ methods as the adapter. Scope behavior has an integration test. Keep unrelated
97
+ CRUD/memory/archive methods together until a second adapter or concrete
98
+ independent migration/testing seam justifies further division.
99
+ - `response` column via ALTER (SPEC-ISSUES-001).
100
+ - `memories.prompt_ids` possibly unused after summaries (SPEC-ISSUES-014).
101
+ - `delete` (427) and `deleteById` (435) coexist — duplicate deletion paths.
102
+
103
+ ## 10. Related
104
+ - SPEC-005 (search), SPEC-007 (memory), SPEC-009 (retention), SPEC-010 (sync).
105
+ Public behavior is covered by Vitest integration tests (SPEC-ISSUES-007).
@@ -0,0 +1,110 @@
1
+ # SPEC-003: Capture Modes (PTY / Inline / Hooks / Log)
2
+
3
+ - **ID**: SPEC-003
4
+ - **Cluster**: Capture
5
+ - **Status**: Implemented (baseline)
6
+ - **Date**: 2026-08-08
7
+ - **Source**: reverse-engineered from code (files listed in §8)
8
+
9
+ ## 1. Purpose
10
+ The three ways a prompt+response reaches the DB: transparent PTY wrapping of an
11
+ interactive tool, inline (non-interactive) capture, and direct logging via hooks
12
+ or `ph log`. Plus the OpenCode plugin for real-time capture.
13
+
14
+ ## 2. Scope
15
+ - **In scope**: runPTY (ESC/OSC state machine), runInline, resolveRealBinary,
16
+ ph log command, hooks, opencode plugin.
17
+ - **Out of scope**: what happens after capture (analysis SPEC-006, background
18
+ push SPEC-010).
19
+ - **Entry points**: `ph <tool> …` (wrapper), `ph log`, hook scripts, opencode plugin.
20
+
21
+ ## 3. Data Model
22
+ - `PromptCallback(prompt, timestamp) => id` and `ResponseCallback(id, response)`
23
+ (src/pty/wrapper.ts:4-5).
24
+ - Entry shape: `Omit<PromptEntry,'id'|'response'> & {response?: string}`
25
+ (PhDB.insert, SPEC-002).
26
+ - Live captures and imports use `createCaptureRecord` (src/capture/index.ts)
27
+ to normalize timestamps, host, project/language/title metadata, and source
28
+ fields before persistence.
29
+
30
+ ## 4. Flows
31
+
32
+ ### 4.1 PTY wrapper (src/pty/wrapper.ts:23-169)
33
+ 1. `pty.spawn(binary, args, {name:'xterm-256color', cols, rows, cwd, env})`.
34
+ 2. Forward PTY output to stdout; buffer it as the "response".
35
+ 3. stdin in raw mode; a byte-level ESC/CSI/OSC state machine (wrapper.ts:74-145)
36
+ detects Enter (prompt capture), backspace, Ctrl-C/D, and ignores escape
37
+ sequences. On Enter: finalize previous response, call `onPrompt`.
38
+ 4. On PTY exit: finalize last response, restore stdin, resolve with exitCode.
39
+
40
+ ### 4.2 Inline (src/runner/inline.ts:37)
41
+ `runInline` captures args → `extractPrompt(args)` (:8), runs the binary
42
+ non-interactively (spawnSync), records exit code, then `db.insert`.
43
+
44
+ ### 4.3 Direct log (src/commands/log.ts:9-70)
45
+ `ph log --tool <t> --prompt <p> [--response <r>]` builds the entry (project
46
+ detection, language detection, topic extraction), inserts, then optionally
47
+ spawns background analysis (SPEC-006) and a fire-and-forget remote push
48
+ (SPEC-010).
49
+
50
+ ### 4.4 Hooks + plugin
51
+ - `hooks/claude/ph-hook.sh`, `hooks/gemini/ph-hook.sh` — shell scripts invoked
52
+ post-session; they pipe JSON to `ph log`.
53
+ - `hooks/codex/ph-hook.sh` — Codex `Stop` hook reads `transcript_path` from
54
+ stdin and imports the active rollout with `ph import codex --file`.
55
+ - `hooks/opencode/ph-plugin.ts` — real-time capture via OpenCode hooks
56
+ (`chat.message` + `experimental.text.complete`), pairs prompts with streamed
57
+ responses, calls `ph log` in background; `hooks/opencode/install.sh`.
58
+
59
+ ## 5. Invariants & Business rules
60
+ - PTY capture is byte-exact for prompts: ANSI sequences never leak into the
61
+ prompt text (ESC state machine).
62
+ - `--ph-debug` writes a session log to `~/.ph_debug.log` via `debugLog`.
63
+ - Capture never blocks the wrapped tool (background processes for analysis/push).
64
+ - Binary resolution: `resolveRealBinary(tool)` maps known tool names
65
+ (src/runner/inline.ts:15).
66
+ - All supported capture adapters produce the same normalized PromptEntry shape;
67
+ source-specific metadata remains attached to that record.
68
+
69
+ ## 6. UI / UX surface
70
+ Transparent: the wrapped tool's own output is forwarded verbatim; ph itself
71
+ prints only capture feedback.
72
+
73
+ ## 7. Acceptance criteria (Given/When/Then)
74
+ - **G1**: Given a PTY session where the user types `hello` and presses Enter,
75
+ When the ESC state machine processes the bytes, Then `onPrompt` fires with
76
+ `"hello"` (src/pty/wrapper.ts:87-98).
77
+ - **G2**: Given ANSI/OSC sequences (e.g. `\x1b]0;title\x07`), When the state
78
+ machine runs, Then the sequences are consumed and never appended to
79
+ `lineBuffer` (src/pty/wrapper.ts:114-143).
80
+ - **G3**: Given `ph log --tool claude --prompt "x"` without `--response`, When
81
+ cmdLog runs, Then the entry is inserted with `response: ''` and project
82
+ metadata (src/commands/log.ts, SPEC-002 G2).
83
+ - **G4**: Given `extractPrompt(["explain","goroutines"])`, When run, Then the
84
+ joined prompt text is returned (src/runner/inline.ts:8-14).
85
+ - **G5**: Given a capture with a project marker and adapter metadata, When
86
+ `createCaptureRecord` builds it, Then it includes detected project/language,
87
+ source metadata, and common fields (src/capture/index.ts).
88
+
89
+ ## 8. Key implementation map
90
+ | Concern | File(s) |
91
+ |---|---|
92
+ | PTY + ESC state machine | src/pty/wrapper.ts:23-169 |
93
+ | Inline runner + binary resolution | src/runner/inline.ts:8-60 |
94
+ | Direct log | src/commands/log.ts:9-85 |
95
+ | Normalized capture record | src/capture/index.ts |
96
+ | Hooks | hooks/claude/ph-hook.sh, hooks/gemini/ph-hook.sh, hooks/codex/ph-hook.sh |
97
+ | OpenCode plugin | hooks/opencode/ph-plugin.ts, install.sh |
98
+
99
+ ## 9. Open questions / discrepancies
100
+ - `cleanLine` (wrapper.ts:7-10) trims whitespace — prompts containing leading/
101
+ trailing spaces or multi-line pastes are collapsed; paste detection (bracketed
102
+ paste) is not implemented.
103
+ - The PTY path is not exercised by any test (requires a PTY); the state machine
104
+ logic is pure enough to unit-test with byte arrays.
105
+
106
+ ## 10. Related
107
+ - SPEC-001 (wrapper-mode dispatch), SPEC-002 (insert), SPEC-006 (background
108
+ analysis), SPEC-010 (background push). The normalized capture record is
109
+ covered by `src/capture/__tests__/record.test.ts`; PTY/integration coverage
110
+ remains open (SPEC-ISSUES-007).
@@ -0,0 +1,82 @@
1
+ # SPEC-004: Filter Pipeline & Metadata
2
+
3
+ - **ID**: SPEC-004
4
+ - **Cluster**: Core
5
+ - **Status**: Implemented (baseline)
6
+ - **Date**: 2026-08-08
7
+ - **Source**: reverse-engineered from code (files listed in §8)
8
+
9
+ ## 1. Purpose
10
+ Keeps noise out of the history: `FilterPipeline` rejects too-short, binary,
11
+ trivial and duplicate prompts before they reach the DB, and enriches entries
12
+ with project/language/topic metadata at capture time.
13
+
14
+ ## 2. Scope
15
+ - **In scope**: FilterPipeline checks, FilterResult, metadata enrichment
16
+ (detectProject, detectLanguage, extractTopic), filter config knobs.
17
+ - **Out of scope**: LLM analysis (SPEC-006), retention (SPEC-009).
18
+ - **Entry points**: FilterPipeline used by `ph log` / importers; metadata
19
+ helpers used across commands.
20
+
21
+ ## 3. Data Model
22
+ - `FilterReason` union + `FilterResult` (src/filter/index.ts:4-11): `{ pass,
23
+ reason? }`.
24
+ - `FilterOptions` (src/filter/index.ts:17): minLength, minRelevance.
25
+ - Metadata JSON shape `PromptMetadata` (src/types.ts:14-27).
26
+
27
+ ## 4. Flows
28
+
29
+ ### 4.1 Filtering (src/filter/index.ts:34)
30
+ `FilterPipeline.check(text)` runs 4 checks (AGENTS.md + filter/index.ts):
31
+ 1. too short (< `filterMinLength`, default 15)
32
+ 2. non-printable characters
33
+ 3. trivial pattern (e.g. single words, "ok", "yes")
34
+ 4. exact duplicate (against recent history)
35
+ Optional relevance check when `minRelevance > 0`.
36
+
37
+ ### 4.2 Enrichment
38
+ - `detectProject(startDir)` walks up looking for `.git`/`go.mod`/`package.json`/
39
+ `Cargo.toml` (src/runner/project.ts:25).
40
+ - `detectLanguage(rootDir)` (src/runner/project.ts:40).
41
+ - `extractTopic(prompt)` (src/utils/extractTopic.ts:5) — title-like extraction.
42
+ - Assembled into `metadata` JSON before insert (e.g. src/commands/log.ts:50-58).
43
+
44
+ ## 5. Invariants & Business rules
45
+ - Filtered prompts never reach the DB (importers and log both call the pipeline).
46
+ - Metadata merge later (SPEC-006) preserves manual fields unless `force`.
47
+ - `filterMinRelevance` default 3 — but relevance only exists after analysis, so
48
+ the check only bites on re-analysis paths.
49
+
50
+ ## 6. UI / UX surface
51
+ Filter counts/reasons are logged at capture time; configurable via
52
+ `ph config set filter-min-length <n>` etc.
53
+
54
+ ## 7. Acceptance criteria (Given/When/Then)
55
+ - **G1**: Given a 5-char prompt with `minLength: 15`, When `FilterPipeline.check`
56
+ runs, Then `{ pass: false, reason: 'too short' }` (src/filter/index.ts:34+).
57
+ - **G2**: Given a prompt with control bytes, When `check` runs, Then it is
58
+ rejected as non-printable (src/filter/index.ts).
59
+ - **G3**: Given a prompt equal to an existing recent entry, When `check` runs,
60
+ Then it is rejected as duplicate (src/filter/index.ts).
61
+ - **G4**: Given a dir containing `package.json`, When `detectProject` runs, Then
62
+ the project name is derived from the package (src/runner/project.ts:25-39).
63
+ - **G5**: Given `extractTopic("fix the login bug")`, When run, Then a short
64
+ topic string is returned (src/utils/extractTopic.ts:5).
65
+
66
+ ## 8. Key implementation map
67
+ | Concern | File(s) |
68
+ |---|---|
69
+ | FilterPipeline + reasons | src/filter/index.ts:4-80 |
70
+ | Project/language detection | src/runner/project.ts:25-60 |
71
+ | Topic extraction | src/utils/extractTopic.ts:5 |
72
+ | Capture-time assembly | src/commands/log.ts:50-58 |
73
+
74
+ ## 9. Open questions / discrepancies
75
+ - The exact trivial-pattern list lives inline in filter/index.ts; verify the
76
+ regexes when wiring GWT tests.
77
+ - Duplicate check scope (recent history window vs all) is implementation detail
78
+ in filter/index.ts — confirm before writing the G3 test.
79
+
80
+ ## 10. Related
81
+ - SPEC-002 (insert), SPEC-003 (capture), SPEC-013 (importers reuse the filter),
82
+ SPEC-015 (config keys). No tests exist (SPEC-ISSUES-007).
@@ -0,0 +1,102 @@
1
+ # SPEC-005: Search (FTS + Filters + Semantic)
2
+
3
+ - **ID**: SPEC-005
4
+ - **Cluster**: Search
5
+ - **Status**: Implemented (baseline)
6
+ - **Date**: 2026-08-08
7
+ - **Source**: reverse-engineered from code (files listed in §8)
8
+
9
+ ## 1. Purpose
10
+ Find prompts by full-text (FTS5 over prompt+response), by metadata filters
11
+ (tool/project/language/role/tag/starred/quality/relevance/date range), or by
12
+ semantic similarity (sqlite-vec, SPEC-008). Backs `ph search`, the TUI text
13
+ filter, the MCP `search_prompts*` tools and the HTTP `/api/prompts/search`.
14
+
15
+ ## 2. Scope
16
+ - **In scope**: `PhDB.search(opts)` dual-path (FTS vs scan), filter building,
17
+ semantic search, `ph search` command output.
18
+ - **Out of scope**: embeddings generation (SPEC-008), archive search (SPEC-009),
19
+ TUI filter panel (SPEC-012).
20
+ - **Entry points**: `ph search [--semantic] [--archive] [flags]`, TUI `/`, MCP tools, HTTP endpoints.
21
+
22
+ ## 3. Data Model
23
+ - `SearchOptions` (src/types.ts:54-68): query, tool, project, language, role,
24
+ tag, starred, minQuality, minRelevance, since, until, limit, semantic.
25
+ - Results: `PromptEntry[]`.
26
+
27
+ ## 4. Flows
28
+
29
+ ### 4.1 Dual-path query (src/db/index.ts:243-330)
30
+ 1. Build identical filters for two paths: FTS (JOIN with alias `p.`) and scan
31
+ (direct `prompts`). Filters use `json_extract` on metadata for
32
+ project/language/role/tag/starred/quality/relevance (db/index.ts:252-297).
33
+ 2. With a text query → FTS path: `prompts_fts JOIN prompts p` via MATCH, filters
34
+ prefixed `p.` (db/index.ts:300-320).
35
+ 3. Without text → scan path: `SELECT * FROM prompts WHERE … ORDER BY timestamp
36
+ DESC LIMIT ?` (db/index.ts:322-330).
37
+ 4. Date range: `since`/`until` become timestamp comparisons.
38
+
39
+ ### 4.2 Semantic search (src/db/index.ts)
40
+ `searchSemantic(queryVector, limit, project?)` — vec0 KNN on `vec_embeddings`
41
+ joined back to prompts. When `project` is supplied, matching is restricted to
42
+ that project before the final result limit. Requires embeddings (SPEC-008).
43
+ Because sqlite-vec applies `k` before ordinary metadata predicates, scoped
44
+ queries currently examine all stored vectors before filtering and limiting.
45
+
46
+ ### 4.3 CLI (src/commands/search.ts)
47
+ `ph search` prints via `printResults` (src/display/print.ts:33); `-i` opens the
48
+ TUI (SPEC-001); `--archive` queries `prompts_archive` (SPEC-009).
49
+
50
+ ## 5. Invariants & Business rules
51
+ - FTS path is used ONLY when a text query exists; otherwise full scan + filters.
52
+ - `tag` filter is a JSON-array LIKE `%"tag"%` — substring match on the serialized
53
+ array, not a JSON array membership check (db/index.ts:276-282).
54
+ - `limit` defaults to 50 in the CLI; results ordered timestamp DESC.
55
+ - Project-scoped semantic search returns the nearest matching prompts within
56
+ that project, even when closer prompts from other projects exist.
57
+
58
+ ## 6. UI / UX surface
59
+ `ph search` output: `#id tool date [tags] [exit:N]` + truncated prompt
60
+ (printResults); `--full` shows the complete text.
61
+
62
+ ## 7. Acceptance criteria (Given/When/Then)
63
+ - **G1**: Given prompts with text "goroutines" and "react", When `search({query:
64
+ "goroutines"})` runs, Then only the goroutines entry is returned via the FTS
65
+ path (src/db/index.ts:300-320).
66
+ - **G2**: Given prompts from tools claude/gemini, When `search({tool: "gemini"})`
67
+ runs (no query), Then only gemini entries are returned via the scan path
68
+ (src/db/index.ts:322-330).
69
+ - **G3**: Given entries with metadata `project: "ph"`, When
70
+ `search({project: "ph"})` runs, Then only those entries match via
71
+ `json_extract` (src/db/index.ts:258-263).
72
+ - **G4**: Given a tag `auth` stored in `tags: ["auth","jwt"]`, When
73
+ `search({tag: "auth"})` runs, Then the entry matches via the LIKE filter
74
+ (src/db/index.ts:276-282).
75
+ - **G5**: Given `since: 2026-01-01`, When `search` runs, Then entries older than
76
+ the date are excluded (src/db/index.ts:since handling).
77
+ - **G6**: Given an unrelated project has the nearest vector, When
78
+ `searchSemantic(query, 1, "go-play")` runs, Then it returns the nearest
79
+ vector belonging to `go-play` (src/db/index.ts, project scope before limit).
80
+
81
+ ## 8. Key implementation map
82
+ | Concern | File(s) |
83
+ |---|---|
84
+ | Dual-path search | src/db/index.ts:243-330 |
85
+ | Semantic KNN | src/db/index.ts (`searchSemantic`) |
86
+ | CLI output | src/commands/search.ts, src/display/print.ts:33-60 |
87
+ | SearchOptions | src/types.ts:54-68 |
88
+
89
+ ## 9. Open questions / discrepancies
90
+ - `tag` filter uses LIKE on the JSON string — matches `"auth2"` when filtering
91
+ `auth` (substring). JSON array membership would be stricter.
92
+ - FTS path columns need the `p.` prefix; a new filter added to only one path
93
+ silently breaks the other (dual maintenance).
94
+ - Project-scoped semantic search currently evaluates the full vector set
95
+ before metadata filtering because sqlite-vec's KNN limit precedes the join
96
+ predicate. A project-aware vector index or auxiliary metadata column may
97
+ reduce this cost if vector counts make it material.
98
+
99
+ ## 10. Related
100
+ - SPEC-002 (primitives), SPEC-008 (embeddings), SPEC-009 (archive),
101
+ SPEC-011 (MCP), SPEC-010 (HTTP). Vitest coverage: `src/db/__tests__/phdb.test.ts`
102
+ covers G1-G6, including project-scoped nearest-neighbor behavior.
@@ -0,0 +1,98 @@
1
+ # SPEC-006: Analysis Pipeline (LLM)
2
+
3
+ - **ID**: SPEC-006
4
+ - **Cluster**: AI
5
+ - **Status**: Implemented (baseline)
6
+ - **Date**: 2026-08-08
7
+ - **Source**: reverse-engineered from code (files listed in §8)
8
+
9
+ ## 1. Purpose
10
+ Turns raw prompts into structured knowledge: role/tags/relevance/quality plus
11
+ summary/key_insights/technical_decisions via an LLM. Results feed metadata
12
+ (SPEC-002), memories and project summaries (SPEC-007).
13
+
14
+ ## 2. Scope
15
+ - **In scope**: ANALYSIS_PROMPT, parseAnalysisResponse, analyzePrompt, mergeMetadata,
16
+ analyzeAll, background analysis spawn, reusability scoring.
17
+ - **Out of scope**: provider details (src/ai/*), memory persistence (SPEC-007).
18
+ - **Entry points**: `ph analyze`, `ph analyze-reusability`, `_bg-analyze`,
19
+ auto-analysis after `ph log` when `backgroundAnalysis` is enabled.
20
+
21
+ ## 3. Data Model
22
+ - `AnalysisResult` (src/analyzer/index.ts:8-18): project, language, role, tags,
23
+ relevance (0-10), quality (0-10), summary, key_insights, technical_decisions.
24
+ - `PromptMetadata` (src/types.ts:14-27) — analysis merged into it.
25
+ - `LLMProvider` interface (src/ai/provider.ts:5-12); factory `getProvider(cfg)`
26
+ (:14) returns Ollama or Gemini.
27
+
28
+ ## 4. Flows
29
+
30
+ ### 4.1 Prompt (src/analyzer/index.ts:20-50)
31
+ `ANALYSIS_PROMPT(text)` demands a single JSON object with the exact field
32
+ contract; input truncated to 2000 chars (:49). No markdown fences allowed.
33
+
34
+ ### 4.2 Parsing (src/analyzer/index.ts:56-120)
35
+ `parseAnalysisResponse` strips fences, extracts the first `{…}` block
36
+ (indexOf/lastIndexOf), JSON.parses; on failure returns `{}` (never throws).
37
+
38
+ ### 4.3 Analyze + merge (src/analyzer/index.ts:123-183)
39
+ `analyzePrompt(text, provider)` → `parseAnalysisResponse(await provider.analyze…)`.
40
+ `mergeMetadata(existing, analysis, force?)` merges fields, preserving manual
41
+ metadata unless `force=true` (:136-183).
42
+
43
+ ### 4.4 Batch (src/analyzer/index.ts:205)
44
+ `analyzeAll(db, provider, opts)` iterates unanalyzed prompts, updates metadata,
45
+ writes memories (SPEC-007). CLI `ph analyze` with `--limit/--force/--prune/
46
+ --dry-run`.
47
+
48
+ ### 4.5 Background (src/background/analyzer.ts:7)
49
+ `spawnBackgroundAnalysis(id, dbPath)` spawns a detached `_bg-analyze` child —
50
+ never blocks the capture path. Enabled via `ph config set background-analysis true`.
51
+
52
+ ### 4.6 Reusability (src/analyzer/reusability.ts)
53
+ `ph analyze-reusability` scores prompts for reuse; `cleanup-reusability`
54
+ deletes low-score ones (SPEC-009 cross-ref).
55
+
56
+ ## 5. Invariants & Business rules
57
+ - Parsing NEVER throws — worst case is an empty AnalysisResult.
58
+ - Analysis is additive: manual fields survive unless `force`.
59
+ - Background analysis is detached: capture latency is never affected.
60
+ - Input truncated to 2000 chars before the LLM.
61
+
62
+ ## 6. UI / UX surface
63
+ `ph analyze` progress output; TUI shows `●` (analyzed) vs `○` (pending) in
64
+ ListEntry (SPEC-012).
65
+
66
+ ## 7. Acceptance criteria (Given/When/Then)
67
+ - **G1**: Given an LLM response with fenced JSON, When `parseAnalysisResponse`
68
+ runs, Then the JSON is extracted and parsed (src/analyzer/index.ts:56-65).
69
+ - **G2**: Given a non-JSON response, When `parseAnalysisResponse` runs, Then
70
+ `{}` is returned without throwing (src/analyzer/index.ts:61-70).
71
+ - **G3**: Given existing metadata with a manual role and analysis with a
72
+ different role, When `mergeMetadata(existing, analysis)` runs without force,
73
+ Then the manual role is preserved (src/analyzer/index.ts:136-183).
74
+ - **G4**: Given `force=true`, When `mergeMetadata` runs, Then the analysis
75
+ values win (src/analyzer/index.ts:136-183).
76
+ - **G5**: Given `backgroundAnalysis: true`, When `ph log` runs, Then
77
+ `spawnBackgroundAnalysis` is invoked with the new prompt id and dbPath
78
+ (src/commands/log.ts:61-63, src/background/analyzer.ts:7).
79
+
80
+ ## 8. Key implementation map
81
+ | Concern | File(s) |
82
+ |---|---|
83
+ | Prompt + parse + merge + batch | src/analyzer/index.ts:20-275 |
84
+ | Providers + factory | src/ai/provider.ts:5-20, ollama.ts:8, gemini.ts:36 |
85
+ | Background spawn | src/background/analyzer.ts:7 |
86
+ | Reusability | src/analyzer/reusability.ts |
87
+
88
+ ## 9. Open questions / discrepancies
89
+ - `parseAnalysisResponse` uses first/last brace — nested braces or multiple JSON
90
+ blocks break extraction (same fragility class as lens's old regex plans).
91
+ - `_bg-analyze` passes only (id, dbPath): the child reloads config; a config
92
+ change between capture and analysis silently changes the model used.
93
+ - Reusability scoring lives in analyzer/reusability.ts — verify the formula when
94
+ wiring tests.
95
+
96
+ ## 10. Related
97
+ - SPEC-002 (metadata update), SPEC-007 (memories), SPEC-015 (config),
98
+ SPEC-004 (filter runs before analysis). No tests (SPEC-ISSUES-007).