thumbgate 0.9.10
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.
- package/.claude-plugin/README.md +134 -0
- package/.claude-plugin/bundle/icon.png +0 -0
- package/.claude-plugin/bundle/icon.svg +18 -0
- package/.claude-plugin/bundle/server/index.js +24 -0
- package/.claude-plugin/marketplace.json +36 -0
- package/.claude-plugin/plugin.json +21 -0
- package/.well-known/mcp/server-card.json +231 -0
- package/LICENSE +21 -0
- package/README.md +375 -0
- package/adapters/README.md +9 -0
- package/adapters/amp/skills/thumbgate-feedback/SKILL.md +22 -0
- package/adapters/chatgpt/INSTALL.md +83 -0
- package/adapters/chatgpt/openapi.yaml +1281 -0
- package/adapters/claude/.mcp.json +14 -0
- package/adapters/codex/config.toml +9 -0
- package/adapters/gemini/function-declarations.json +224 -0
- package/adapters/mcp/server-stdio.js +788 -0
- package/adapters/opencode/opencode.json +15 -0
- package/bin/cli.js +1484 -0
- package/bin/memory.sh +64 -0
- package/bin/obsidian-sync.sh +20 -0
- package/bin/postinstall.js +37 -0
- package/config/build-metadata.json +4 -0
- package/config/e2e-critical-flows.json +45 -0
- package/config/gate-templates.json +77 -0
- package/config/gates/claim-verification.json +29 -0
- package/config/gates/computer-use.json +39 -0
- package/config/gates/default.json +117 -0
- package/config/github-about.json +25 -0
- package/config/mcp-allowlists.json +135 -0
- package/config/model-tiers.json +33 -0
- package/config/partner-routing.json +132 -0
- package/config/policy-bundles/constrained-v1.json +64 -0
- package/config/policy-bundles/default-v1.json +91 -0
- package/config/rubrics/default-v1.json +52 -0
- package/config/skill-packs/react-testing.json +23 -0
- package/config/skill-packs/stripe-integration/references/api-spec.json +1 -0
- package/config/skill-packs/stripe-integration/references/webhook-guide.md +3 -0
- package/config/skill-specs/pr-reviewer.json +9 -0
- package/config/skill-specs/release-status.json +9 -0
- package/config/skill-specs/ticket-triage.json +9 -0
- package/config/subagent-profiles.json +32 -0
- package/config/tessl-tiles.json +29 -0
- package/config/thumbgate-settings.managed.json +12 -0
- package/openapi/openapi.yaml +1281 -0
- package/package.json +283 -0
- package/plugins/amp-skill/INSTALL.md +52 -0
- package/plugins/amp-skill/SKILL.md +64 -0
- package/plugins/claude-codex-bridge/.claude-plugin/plugin.json +22 -0
- package/plugins/claude-codex-bridge/.mcp.json +12 -0
- package/plugins/claude-codex-bridge/INSTALL.md +43 -0
- package/plugins/claude-codex-bridge/README.md +46 -0
- package/plugins/claude-codex-bridge/scripts/codex-bridge.js +288 -0
- package/plugins/claude-codex-bridge/skills/adversarial-review/SKILL.md +24 -0
- package/plugins/claude-codex-bridge/skills/result/SKILL.md +22 -0
- package/plugins/claude-codex-bridge/skills/review/SKILL.md +28 -0
- package/plugins/claude-codex-bridge/skills/second-pass/SKILL.md +27 -0
- package/plugins/claude-codex-bridge/skills/setup/SKILL.md +21 -0
- package/plugins/claude-codex-bridge/skills/status/SKILL.md +19 -0
- package/plugins/claude-skill/INSTALL.md +55 -0
- package/plugins/claude-skill/SKILL.md +46 -0
- package/plugins/codex-profile/.codex-plugin/plugin.json +43 -0
- package/plugins/codex-profile/.mcp.json +12 -0
- package/plugins/codex-profile/AGENTS.md +20 -0
- package/plugins/codex-profile/INSTALL.md +66 -0
- package/plugins/codex-profile/README.md +37 -0
- package/plugins/cursor-marketplace/.cursor-plugin/plugin.json +23 -0
- package/plugins/cursor-marketplace/CHANGELOG.md +30 -0
- package/plugins/cursor-marketplace/LICENSE +21 -0
- package/plugins/cursor-marketplace/README.md +124 -0
- package/plugins/cursor-marketplace/agents/reliability-reviewer.md +31 -0
- package/plugins/cursor-marketplace/assets/logo-400x400.png +0 -0
- package/plugins/cursor-marketplace/commands/capture-feedback.md +33 -0
- package/plugins/cursor-marketplace/commands/check-gates.md +25 -0
- package/plugins/cursor-marketplace/commands/show-lessons.md +27 -0
- package/plugins/cursor-marketplace/hooks/hooks.json +10 -0
- package/plugins/cursor-marketplace/mcp.json +12 -0
- package/plugins/cursor-marketplace/rules/feedback-capture.mdc +34 -0
- package/plugins/cursor-marketplace/rules/pre-action-gates.mdc +30 -0
- package/plugins/cursor-marketplace/rules/session-continuity.mdc +28 -0
- package/plugins/cursor-marketplace/scripts/gate-check.sh +11 -0
- package/plugins/cursor-marketplace/skills/capture-feedback/SKILL.md +47 -0
- package/plugins/cursor-marketplace/skills/prevention-rules/SKILL.md +31 -0
- package/plugins/cursor-marketplace/skills/recall-context/SKILL.md +30 -0
- package/plugins/cursor-marketplace/skills/search-lessons/SKILL.md +33 -0
- package/plugins/gemini-extension/INSTALL.md +92 -0
- package/plugins/gemini-extension/gemini_prompt.txt +14 -0
- package/plugins/gemini-extension/tool_contract.json +45 -0
- package/plugins/opencode-profile/INSTALL.md +57 -0
- package/public/assets/instagram-card.png +0 -0
- package/public/assets/tiktok-agent-memory.mp4 +0 -0
- package/public/blog.html +400 -0
- package/public/dashboard.html +1093 -0
- package/public/guide.html +317 -0
- package/public/index.html +1014 -0
- package/public/learn/agent-harness-pattern.html +180 -0
- package/public/learn/ai-agent-persistent-memory.html +202 -0
- package/public/learn/learn.css +45 -0
- package/public/learn/mcp-pre-action-gates-explained.html +172 -0
- package/public/learn/stop-ai-agent-force-push.html +134 -0
- package/public/learn/vibe-coding-safety-net.html +142 -0
- package/public/learn.html +213 -0
- package/public/lessons.html +650 -0
- package/public/vercel.json +8 -0
- package/scripts/__pycache__/train_from_feedback.cpython-312.pyc +0 -0
- package/scripts/a2ui-engine.js +73 -0
- package/scripts/access-anomaly-detector.js +12 -0
- package/scripts/adk-consolidator.js +266 -0
- package/scripts/agent-readiness.js +220 -0
- package/scripts/agent-security-hardening.js +227 -0
- package/scripts/agentic-data-pipeline.js +847 -0
- package/scripts/analytics-report.js +328 -0
- package/scripts/analytics-window.js +158 -0
- package/scripts/async-job-runner.js +1001 -0
- package/scripts/audit-trail.js +398 -0
- package/scripts/auto-promote-gates.js +299 -0
- package/scripts/auto-wire-hooks.js +312 -0
- package/scripts/autonomous-sales-agent.js +39 -0
- package/scripts/autoresearch-runner.js +216 -0
- package/scripts/background-agent-governance.js +237 -0
- package/scripts/behavioral-extraction.js +97 -0
- package/scripts/belief-update.js +84 -0
- package/scripts/billing.js +2438 -0
- package/scripts/bot-detector.js +50 -0
- package/scripts/budget-guard.js +173 -0
- package/scripts/build-claude-mcpb.js +189 -0
- package/scripts/build-metadata.js +97 -0
- package/scripts/check-congruence.js +322 -0
- package/scripts/cli-feedback.js +135 -0
- package/scripts/cli-telemetry.js +87 -0
- package/scripts/cloudflare-dynamic-sandbox.js +315 -0
- package/scripts/code-reasoning.js +350 -0
- package/scripts/codegraph-context.js +466 -0
- package/scripts/commercial-offer.js +56 -0
- package/scripts/computer-use-firewall.js +250 -0
- package/scripts/context-engine.js +694 -0
- package/scripts/contextfs.js +1287 -0
- package/scripts/conversation-context.js +119 -0
- package/scripts/creator-campaigns.js +239 -0
- package/scripts/daemon-manager.js +108 -0
- package/scripts/daily-digest.js +11 -0
- package/scripts/dashboard-render-spec.js +395 -0
- package/scripts/dashboard.js +1058 -0
- package/scripts/data-governance.js +173 -0
- package/scripts/delegation-runtime.js +900 -0
- package/scripts/deploy-gcp.sh +44 -0
- package/scripts/deploy-policy.js +263 -0
- package/scripts/disagreement-mining.js +315 -0
- package/scripts/dispatch-brief.js +159 -0
- package/scripts/distribution-surfaces.js +44 -0
- package/scripts/dpo-optimizer.js +209 -0
- package/scripts/ephemeral-agent-store.js +219 -0
- package/scripts/eval-harness.js +56 -0
- package/scripts/evolution-state.js +241 -0
- package/scripts/experiment-tracker.js +267 -0
- package/scripts/export-databricks-bundle.js +242 -0
- package/scripts/export-dpo-pairs.js +345 -0
- package/scripts/export-kto-pairs.js +310 -0
- package/scripts/export-training.js +448 -0
- package/scripts/failure-diagnostics.js +558 -0
- package/scripts/feedback-attribution.js +313 -0
- package/scripts/feedback-fallback.js +111 -0
- package/scripts/feedback-history-distiller.js +391 -0
- package/scripts/feedback-inbox-read.js +162 -0
- package/scripts/feedback-loop.js +1887 -0
- package/scripts/feedback-paths.js +145 -0
- package/scripts/feedback-quality.js +139 -0
- package/scripts/feedback-root-consolidator.js +238 -0
- package/scripts/feedback-schema.js +426 -0
- package/scripts/feedback-session.js +286 -0
- package/scripts/feedback-to-memory.js +185 -0
- package/scripts/feedback-to-rules.js +163 -0
- package/scripts/filesystem-search.js +404 -0
- package/scripts/funnel-analytics.js +35 -0
- package/scripts/gate-satisfy.js +42 -0
- package/scripts/gate-stats.js +116 -0
- package/scripts/gate-templates.js +70 -0
- package/scripts/gates-engine.js +816 -0
- package/scripts/generate-paperbanana-diagrams.sh +99 -0
- package/scripts/generate-pretool-hook.sh +40 -0
- package/scripts/github-about.js +350 -0
- package/scripts/github-outreach.js +65 -0
- package/scripts/gtm-revenue-loop.js +520 -0
- package/scripts/hallucination-detector.js +226 -0
- package/scripts/hf-papers.js +317 -0
- package/scripts/history-distiller.js +200 -0
- package/scripts/hook-auto-capture.sh +95 -0
- package/scripts/hook-stop-pr-thread-check.sh +68 -0
- package/scripts/hook-stop-self-score.sh +51 -0
- package/scripts/hook-stop-verify-deploy.sh +31 -0
- package/scripts/hook-thumbgate-cache-updater.js +48 -0
- package/scripts/hook-verify-before-done.sh +20 -0
- package/scripts/hosted-config.js +170 -0
- package/scripts/hybrid-feedback-context.js +676 -0
- package/scripts/install-mcp.js +159 -0
- package/scripts/intent-router.js +392 -0
- package/scripts/internal-agent-bootstrap.js +490 -0
- package/scripts/jsonl-watcher.js +155 -0
- package/scripts/lesson-db.js +613 -0
- package/scripts/lesson-inference.js +315 -0
- package/scripts/lesson-retrieval.js +95 -0
- package/scripts/lesson-rotation.js +137 -0
- package/scripts/lesson-search.js +644 -0
- package/scripts/lesson-synthesis.js +196 -0
- package/scripts/license.js +50 -0
- package/scripts/local-model-profile.js +383 -0
- package/scripts/markdown-escape.js +12 -0
- package/scripts/marketing-experiment.js +671 -0
- package/scripts/mcp-config.js +149 -0
- package/scripts/mcp-policy.js +99 -0
- package/scripts/memalign-recall.js +111 -0
- package/scripts/memory-firewall.js +222 -0
- package/scripts/memory-migration.js +296 -0
- package/scripts/meta-policy.js +194 -0
- package/scripts/metered-billing.js +16 -0
- package/scripts/model-tier-router.js +301 -0
- package/scripts/money-watcher.js +71 -0
- package/scripts/multi-hop-recall.js +240 -0
- package/scripts/natural-language-harness.js +330 -0
- package/scripts/obsidian-export.js +712 -0
- package/scripts/operational-dashboard.js +103 -0
- package/scripts/operational-summary.js +93 -0
- package/scripts/optimize-context.js +17 -0
- package/scripts/org-dashboard.js +201 -0
- package/scripts/partner-orchestration.js +146 -0
- package/scripts/per-step-scoring.js +165 -0
- package/scripts/perplexity-marketing.js +466 -0
- package/scripts/pii-scanner.js +153 -0
- package/scripts/plan-gate.js +154 -0
- package/scripts/post-everywhere.js +308 -0
- package/scripts/post-to-x-retry.sh +22 -0
- package/scripts/post-to-x.js +369 -0
- package/scripts/pr-manager.js +236 -0
- package/scripts/predictive-insights.js +356 -0
- package/scripts/principle-extractor.js +162 -0
- package/scripts/pro-features.js +40 -0
- package/scripts/pro-local-dashboard.js +174 -0
- package/scripts/problem-detail.js +53 -0
- package/scripts/product-feedback.js +134 -0
- package/scripts/profile-router.js +245 -0
- package/scripts/prompt-dlp.js +221 -0
- package/scripts/prompt-guard.js +83 -0
- package/scripts/prove-adapters.js +863 -0
- package/scripts/prove-attribution.js +365 -0
- package/scripts/prove-automation.js +653 -0
- package/scripts/prove-autoresearch.js +304 -0
- package/scripts/prove-claim-verification.js +277 -0
- package/scripts/prove-cloudflare-sandbox.js +163 -0
- package/scripts/prove-data-pipeline.js +410 -0
- package/scripts/prove-data-quality.js +227 -0
- package/scripts/prove-evolution.js +352 -0
- package/scripts/prove-harnesses.js +287 -0
- package/scripts/prove-intelligence.js +259 -0
- package/scripts/prove-lancedb.js +371 -0
- package/scripts/prove-local-intelligence.js +342 -0
- package/scripts/prove-loop-closure.js +263 -0
- package/scripts/prove-predictive-insights.js +357 -0
- package/scripts/prove-runtime.js +350 -0
- package/scripts/prove-seo-gsd.js +234 -0
- package/scripts/prove-settings.js +279 -0
- package/scripts/prove-subway-upgrades.js +277 -0
- package/scripts/prove-tessl.js +229 -0
- package/scripts/prove-training-export.js +327 -0
- package/scripts/prove-workflow-contract.js +116 -0
- package/scripts/prove-xmemory.js +332 -0
- package/scripts/publish-decision.js +133 -0
- package/scripts/pulse.js +80 -0
- package/scripts/rate-limiter.js +125 -0
- package/scripts/reddit-dm-outreach.js +182 -0
- package/scripts/reddit-monitor-cron.sh +26 -0
- package/scripts/reflector-agent.js +221 -0
- package/scripts/reminder-engine.js +132 -0
- package/scripts/revenue-status.js +472 -0
- package/scripts/risk-scorer.js +458 -0
- package/scripts/rlaif-self-audit.js +129 -0
- package/scripts/rubric-engine.js +230 -0
- package/scripts/schedule-manager.js +251 -0
- package/scripts/secret-scanner.js +414 -0
- package/scripts/self-heal.js +147 -0
- package/scripts/self-healing-check.js +188 -0
- package/scripts/semantic-layer.js +98 -0
- package/scripts/seo-gsd.js +1153 -0
- package/scripts/settings-hierarchy.js +214 -0
- package/scripts/shieldcortex-memory-firewall-runner.mjs +53 -0
- package/scripts/skill-exporter.js +262 -0
- package/scripts/skill-generator.js +446 -0
- package/scripts/skill-materializer.js +134 -0
- package/scripts/skill-packs.js +136 -0
- package/scripts/skill-proposer.js +99 -0
- package/scripts/skill-quality-tracker.js +284 -0
- package/scripts/slo-alert-engine.js +14 -0
- package/scripts/slow-loop.js +72 -0
- package/scripts/social-analytics/db/schema.sql +32 -0
- package/scripts/social-analytics/digest.js +256 -0
- package/scripts/social-analytics/generate-instagram-card.js +97 -0
- package/scripts/social-analytics/instagram-thumbgate-post.js +73 -0
- package/scripts/social-analytics/mcp-server.js +289 -0
- package/scripts/social-analytics/normalizer.js +580 -0
- package/scripts/social-analytics/notify.js +162 -0
- package/scripts/social-analytics/poll-all.js +107 -0
- package/scripts/social-analytics/pollers/github.js +195 -0
- package/scripts/social-analytics/pollers/instagram.js +253 -0
- package/scripts/social-analytics/pollers/linkedin.js +330 -0
- package/scripts/social-analytics/pollers/plausible.js +247 -0
- package/scripts/social-analytics/pollers/reddit.js +306 -0
- package/scripts/social-analytics/pollers/threads.js +233 -0
- package/scripts/social-analytics/pollers/tiktok.js +203 -0
- package/scripts/social-analytics/pollers/x.js +227 -0
- package/scripts/social-analytics/pollers/youtube.js +304 -0
- package/scripts/social-analytics/pollers/zernio.js +180 -0
- package/scripts/social-analytics/publish-instagram-thumbgate.js +85 -0
- package/scripts/social-analytics/publishers/devto.js +122 -0
- package/scripts/social-analytics/publishers/instagram.js +317 -0
- package/scripts/social-analytics/publishers/linkedin.js +294 -0
- package/scripts/social-analytics/publishers/reddit.js +390 -0
- package/scripts/social-analytics/publishers/threads.js +275 -0
- package/scripts/social-analytics/publishers/tiktok.js +217 -0
- package/scripts/social-analytics/publishers/x.js +259 -0
- package/scripts/social-analytics/publishers/youtube.js +223 -0
- package/scripts/social-analytics/publishers/zernio.js +209 -0
- package/scripts/social-analytics/run-digest.js +34 -0
- package/scripts/social-analytics/store.js +257 -0
- package/scripts/social-analytics/utm.js +143 -0
- package/scripts/social-pipeline.js +2628 -0
- package/scripts/social-quality-gate.js +18 -0
- package/scripts/social-reply-monitor.js +445 -0
- package/scripts/status-dashboard.js +155 -0
- package/scripts/statusline-lesson.js +16 -0
- package/scripts/statusline-tower.js +8 -0
- package/scripts/statusline.sh +116 -0
- package/scripts/stripe-live-status.js +115 -0
- package/scripts/subagent-profiles.js +79 -0
- package/scripts/sync-gh-secrets-from-env.sh +70 -0
- package/scripts/sync-github-about.js +52 -0
- package/scripts/sync-version.js +451 -0
- package/scripts/synthetic-dpo.js +234 -0
- package/scripts/telemetry-analytics.js +821 -0
- package/scripts/tessl-export.js +371 -0
- package/scripts/test-coverage.js +120 -0
- package/scripts/thompson-sampling.js +417 -0
- package/scripts/thumbgate-search.js +189 -0
- package/scripts/tool-kpi-tracker.js +12 -0
- package/scripts/tool-registry.js +811 -0
- package/scripts/train_from_feedback.py +910 -0
- package/scripts/user-profile.js +78 -0
- package/scripts/validate-feedback.js +580 -0
- package/scripts/validate-workflow-contract.js +287 -0
- package/scripts/vector-store.js +198 -0
- package/scripts/verification-loop.js +291 -0
- package/scripts/verify-obsidian-setup.sh +269 -0
- package/scripts/verify-run.js +269 -0
- package/scripts/webhook-delivery.js +62 -0
- package/scripts/weekly-auto-post.js +124 -0
- package/scripts/workflow-runs.js +154 -0
- package/scripts/workflow-sprint-intake.js +475 -0
- package/scripts/workspace-evolver.js +374 -0
- package/scripts/x-autonomous-marketing.js +139 -0
- package/scripts/xmemory-lite.js +405 -0
- package/skills/agent-memory/SKILL.md +97 -0
- package/skills/solve-architecture-autonomy/SKILL.md +17 -0
- package/skills/solve-architecture-autonomy/tool.js +33 -0
- package/skills/thumbgate/SKILL.md +114 -0
- package/skills/thumbgate-feedback/SKILL.md +49 -0
- package/src/api/server.js +4208 -0
|
@@ -0,0 +1,613 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Lesson DB — SQLite + FTS5 backing store for feedback & memories.
|
|
6
|
+
*
|
|
7
|
+
* Dual-written alongside JSONL (source of truth). Provides:
|
|
8
|
+
* - Full-text search via FTS5 (replaces Jaccard token-overlap)
|
|
9
|
+
* - Indexed tag/domain/signal queries
|
|
10
|
+
* - Corrective-action inference from past similar failures
|
|
11
|
+
*
|
|
12
|
+
* @module lesson-db
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require('node:path');
|
|
16
|
+
const fs = require('node:fs');
|
|
17
|
+
|
|
18
|
+
const PROJECT_ROOT = path.join(__dirname, '..');
|
|
19
|
+
const DEFAULT_DB_PATH = path.join(PROJECT_ROOT, '.claude', 'memory', 'lessons.sqlite');
|
|
20
|
+
|
|
21
|
+
/** @returns {import('better-sqlite3').Database} */
|
|
22
|
+
function initDB(dbPath) {
|
|
23
|
+
const Database = require('better-sqlite3');
|
|
24
|
+
const resolvedPath = dbPath || process.env.LESSON_DB_PATH || DEFAULT_DB_PATH;
|
|
25
|
+
|
|
26
|
+
// Ensure parent directory exists
|
|
27
|
+
const dir = path.dirname(resolvedPath);
|
|
28
|
+
if (!fs.existsSync(dir)) {
|
|
29
|
+
fs.mkdirSync(dir, { recursive: true });
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
const db = new Database(resolvedPath);
|
|
33
|
+
db.pragma('busy_timeout = 3000');
|
|
34
|
+
db.pragma('journal_mode = WAL');
|
|
35
|
+
db.pragma('foreign_keys = ON');
|
|
36
|
+
|
|
37
|
+
db.exec(`
|
|
38
|
+
CREATE TABLE IF NOT EXISTS lessons (
|
|
39
|
+
id TEXT PRIMARY KEY,
|
|
40
|
+
signal TEXT NOT NULL CHECK(signal IN ('positive','negative')),
|
|
41
|
+
context TEXT,
|
|
42
|
+
whatWentWrong TEXT,
|
|
43
|
+
whatToChange TEXT,
|
|
44
|
+
whatWorked TEXT,
|
|
45
|
+
domain TEXT,
|
|
46
|
+
tags TEXT,
|
|
47
|
+
rootCause TEXT,
|
|
48
|
+
importance TEXT DEFAULT 'medium',
|
|
49
|
+
skill TEXT,
|
|
50
|
+
timestamp TEXT NOT NULL,
|
|
51
|
+
sourceFeedbackId TEXT,
|
|
52
|
+
pruned INTEGER DEFAULT 0
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_signal ON lessons(signal);
|
|
56
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_domain ON lessons(domain);
|
|
57
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_importance ON lessons(importance);
|
|
58
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_timestamp ON lessons(timestamp);
|
|
59
|
+
CREATE INDEX IF NOT EXISTS idx_lessons_skill ON lessons(skill);
|
|
60
|
+
`);
|
|
61
|
+
|
|
62
|
+
// FTS5 virtual table for full-text search
|
|
63
|
+
db.exec(`
|
|
64
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS lessons_fts USING fts5(
|
|
65
|
+
context, whatWentWrong, whatToChange, whatWorked, rootCause,
|
|
66
|
+
content=lessons,
|
|
67
|
+
content_rowid=rowid
|
|
68
|
+
);
|
|
69
|
+
`);
|
|
70
|
+
|
|
71
|
+
// Triggers to keep FTS in sync with lessons table
|
|
72
|
+
db.exec(`
|
|
73
|
+
CREATE TRIGGER IF NOT EXISTS lessons_ai AFTER INSERT ON lessons BEGIN
|
|
74
|
+
INSERT INTO lessons_fts(rowid, context, whatWentWrong, whatToChange, whatWorked, rootCause)
|
|
75
|
+
VALUES (new.rowid, new.context, new.whatWentWrong, new.whatToChange, new.whatWorked, new.rootCause);
|
|
76
|
+
END;
|
|
77
|
+
|
|
78
|
+
CREATE TRIGGER IF NOT EXISTS lessons_ad AFTER DELETE ON lessons BEGIN
|
|
79
|
+
INSERT INTO lessons_fts(lessons_fts, rowid, context, whatWentWrong, whatToChange, whatWorked, rootCause)
|
|
80
|
+
VALUES ('delete', old.rowid, old.context, old.whatWentWrong, old.whatToChange, old.whatWorked, old.rootCause);
|
|
81
|
+
END;
|
|
82
|
+
|
|
83
|
+
CREATE TRIGGER IF NOT EXISTS lessons_au AFTER UPDATE ON lessons BEGIN
|
|
84
|
+
INSERT INTO lessons_fts(lessons_fts, rowid, context, whatWentWrong, whatToChange, whatWorked, rootCause)
|
|
85
|
+
VALUES ('delete', old.rowid, old.context, old.whatWentWrong, old.whatToChange, old.whatWorked, old.rootCause);
|
|
86
|
+
INSERT INTO lessons_fts(rowid, context, whatWentWrong, whatToChange, whatWorked, rootCause)
|
|
87
|
+
VALUES (new.rowid, new.context, new.whatWentWrong, new.whatToChange, new.whatWorked, new.rootCause);
|
|
88
|
+
END;
|
|
89
|
+
`);
|
|
90
|
+
|
|
91
|
+
// Session search table — stores session notes for cross-session recall
|
|
92
|
+
db.exec(`
|
|
93
|
+
CREATE TABLE IF NOT EXISTS sessions (
|
|
94
|
+
id TEXT PRIMARY KEY,
|
|
95
|
+
project TEXT,
|
|
96
|
+
branch TEXT,
|
|
97
|
+
summary TEXT,
|
|
98
|
+
content TEXT,
|
|
99
|
+
created_at TEXT NOT NULL
|
|
100
|
+
);
|
|
101
|
+
|
|
102
|
+
CREATE VIRTUAL TABLE IF NOT EXISTS sessions_fts USING fts5(
|
|
103
|
+
summary, content,
|
|
104
|
+
content=sessions,
|
|
105
|
+
content_rowid=rowid
|
|
106
|
+
);
|
|
107
|
+
|
|
108
|
+
CREATE TRIGGER IF NOT EXISTS sessions_ai AFTER INSERT ON sessions BEGIN
|
|
109
|
+
INSERT INTO sessions_fts(rowid, summary, content)
|
|
110
|
+
VALUES (new.rowid, new.summary, new.content);
|
|
111
|
+
END;
|
|
112
|
+
|
|
113
|
+
CREATE TRIGGER IF NOT EXISTS sessions_ad AFTER DELETE ON sessions BEGIN
|
|
114
|
+
INSERT INTO sessions_fts(sessions_fts, rowid, summary, content)
|
|
115
|
+
VALUES ('delete', old.rowid, old.summary, old.content);
|
|
116
|
+
END;
|
|
117
|
+
`);
|
|
118
|
+
|
|
119
|
+
return db;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Upsert a session note into SQLite for cross-session search.
|
|
124
|
+
*/
|
|
125
|
+
function upsertSession(db, session) {
|
|
126
|
+
db.prepare(`
|
|
127
|
+
INSERT OR REPLACE INTO sessions (id, project, branch, summary, content, created_at)
|
|
128
|
+
VALUES (?, ?, ?, ?, ?, ?)
|
|
129
|
+
`).run(
|
|
130
|
+
session.id,
|
|
131
|
+
session.project || null,
|
|
132
|
+
session.branch || null,
|
|
133
|
+
session.summary || null,
|
|
134
|
+
session.content || '',
|
|
135
|
+
session.created_at || new Date().toISOString(),
|
|
136
|
+
);
|
|
137
|
+
return session.id;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Full-text search across past sessions using FTS5.
|
|
142
|
+
*/
|
|
143
|
+
function searchSessions(db, query, limit = 10) {
|
|
144
|
+
if (!query || !query.trim()) {
|
|
145
|
+
return db.prepare('SELECT * FROM sessions ORDER BY created_at DESC LIMIT ?').all(limit);
|
|
146
|
+
}
|
|
147
|
+
const ftsQuery = sanitizeFtsQuery(query);
|
|
148
|
+
try {
|
|
149
|
+
return db.prepare(`
|
|
150
|
+
SELECT s.*, rank
|
|
151
|
+
FROM sessions_fts fts
|
|
152
|
+
JOIN sessions s ON s.rowid = fts.rowid
|
|
153
|
+
WHERE sessions_fts MATCH ?
|
|
154
|
+
ORDER BY rank
|
|
155
|
+
LIMIT ?
|
|
156
|
+
`).all(ftsQuery, limit);
|
|
157
|
+
} catch {
|
|
158
|
+
return db.prepare(
|
|
159
|
+
'SELECT * FROM sessions WHERE content LIKE ? OR summary LIKE ? ORDER BY created_at DESC LIMIT ?',
|
|
160
|
+
).all(`%${query}%`, `%${query}%`, limit);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Upsert a lesson (feedback event + optional memory record) into SQLite.
|
|
166
|
+
* Idempotent — uses INSERT OR REPLACE on the primary key.
|
|
167
|
+
*
|
|
168
|
+
* Dedup rules:
|
|
169
|
+
* 1. Skip pruned records (Bayesian entropy indicates contradictory signal)
|
|
170
|
+
* 2. Skip if an existing lesson has identical whatToChange + overlapping tags
|
|
171
|
+
* (bumps the existing record's importance instead)
|
|
172
|
+
* 3. Always store if whatToChange is empty (raw feedback, not actionable yet)
|
|
173
|
+
*/
|
|
174
|
+
function upsertLesson(db, feedbackEvent, memoryRecord) {
|
|
175
|
+
// Rule 1: skip pruned records — they add noise, not signal
|
|
176
|
+
if (memoryRecord?.pruned) {
|
|
177
|
+
return null;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const id = memoryRecord?.id || feedbackEvent.id;
|
|
181
|
+
const signal = feedbackEvent.signal === 'positive' || feedbackEvent.signal === 'up' ? 'positive' : 'negative';
|
|
182
|
+
const tags = Array.isArray(feedbackEvent.tags) ? feedbackEvent.tags : [];
|
|
183
|
+
const tagsJson = JSON.stringify(tags);
|
|
184
|
+
const domain = feedbackEvent.richContext?.domain || 'general';
|
|
185
|
+
const rootCause = feedbackEvent.diagnosis?.rootCauseCategory || null;
|
|
186
|
+
const importance = memoryRecord?.importance || (signal === 'negative' ? 'high' : 'medium');
|
|
187
|
+
const skill = feedbackEvent.skill || null;
|
|
188
|
+
const whatToChange = feedbackEvent.whatToChange || null;
|
|
189
|
+
|
|
190
|
+
// Rule 2: dedup — if an existing lesson has the same whatToChange and shares tags, skip
|
|
191
|
+
if (whatToChange && whatToChange.trim()) {
|
|
192
|
+
const duplicate = findDuplicate(db, whatToChange, tags);
|
|
193
|
+
if (duplicate) {
|
|
194
|
+
// Bump importance if the new one is higher priority
|
|
195
|
+
const PRIORITY = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
196
|
+
if ((PRIORITY[importance] || 0) > (PRIORITY[duplicate.importance] || 0)) {
|
|
197
|
+
db.prepare('UPDATE lessons SET importance = ? WHERE id = ?').run(importance, duplicate.id);
|
|
198
|
+
}
|
|
199
|
+
return null; // deduplicated
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
const stmt = db.prepare(`
|
|
204
|
+
INSERT OR REPLACE INTO lessons
|
|
205
|
+
(id, signal, context, whatWentWrong, whatToChange, whatWorked, domain, tags, rootCause, importance, skill, timestamp, sourceFeedbackId, pruned)
|
|
206
|
+
VALUES
|
|
207
|
+
(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
|
208
|
+
`);
|
|
209
|
+
|
|
210
|
+
stmt.run(
|
|
211
|
+
id,
|
|
212
|
+
signal,
|
|
213
|
+
feedbackEvent.context || null,
|
|
214
|
+
feedbackEvent.whatWentWrong || memoryRecord?.content || null,
|
|
215
|
+
whatToChange,
|
|
216
|
+
feedbackEvent.whatWorked || null,
|
|
217
|
+
domain,
|
|
218
|
+
tagsJson,
|
|
219
|
+
rootCause,
|
|
220
|
+
importance,
|
|
221
|
+
skill,
|
|
222
|
+
feedbackEvent.timestamp || new Date().toISOString(),
|
|
223
|
+
feedbackEvent.id,
|
|
224
|
+
0, // not pruned (we skip pruned above)
|
|
225
|
+
);
|
|
226
|
+
|
|
227
|
+
return id;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
/**
|
|
231
|
+
* Find an existing lesson with identical whatToChange and overlapping tags.
|
|
232
|
+
* Returns the existing row or null.
|
|
233
|
+
*/
|
|
234
|
+
function findDuplicate(db, whatToChange, tags) {
|
|
235
|
+
if (!whatToChange || !whatToChange.trim()) return null;
|
|
236
|
+
|
|
237
|
+
// Exact match on whatToChange text (normalized)
|
|
238
|
+
const normalized = whatToChange.trim().toLowerCase();
|
|
239
|
+
const candidates = db.prepare(
|
|
240
|
+
`SELECT id, importance, tags FROM lessons WHERE LOWER(TRIM(whatToChange)) = ?`,
|
|
241
|
+
).all(normalized);
|
|
242
|
+
|
|
243
|
+
if (candidates.length === 0) return null;
|
|
244
|
+
|
|
245
|
+
// If any candidate shares at least one tag, it's a duplicate
|
|
246
|
+
for (const c of candidates) {
|
|
247
|
+
if (tags.length === 0) return c; // no tags to compare = text match is enough
|
|
248
|
+
const cTags = safeParseTags(c.tags);
|
|
249
|
+
if (tags.some((t) => cTags.includes(t))) return c;
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
return null;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Compact the lesson DB — merge near-duplicate lessons and remove stale entries.
|
|
257
|
+
*
|
|
258
|
+
* Strategy:
|
|
259
|
+
* - Group lessons by normalized whatToChange text
|
|
260
|
+
* - Keep only the most recent + highest importance per group
|
|
261
|
+
* - Delete the rest
|
|
262
|
+
*
|
|
263
|
+
* @returns {{ removed: number, kept: number }}
|
|
264
|
+
*/
|
|
265
|
+
function compactLessons(db) {
|
|
266
|
+
const all = db.prepare('SELECT id, whatToChange, importance, timestamp, tags FROM lessons ORDER BY timestamp DESC').all();
|
|
267
|
+
const seen = new Map(); // normalized whatToChange → best record
|
|
268
|
+
const toDelete = [];
|
|
269
|
+
const PRIORITY = { critical: 4, high: 3, medium: 2, low: 1 };
|
|
270
|
+
|
|
271
|
+
for (const row of all) {
|
|
272
|
+
if (!row.whatToChange || !row.whatToChange.trim()) continue;
|
|
273
|
+
const key = row.whatToChange.trim().toLowerCase();
|
|
274
|
+
|
|
275
|
+
if (!seen.has(key)) {
|
|
276
|
+
seen.set(key, row);
|
|
277
|
+
} else {
|
|
278
|
+
const existing = seen.get(key);
|
|
279
|
+
const existingPri = PRIORITY[existing.importance] || 0;
|
|
280
|
+
const newPri = PRIORITY[row.importance] || 0;
|
|
281
|
+
if (newPri > existingPri) {
|
|
282
|
+
toDelete.push(existing.id);
|
|
283
|
+
seen.set(key, row);
|
|
284
|
+
} else {
|
|
285
|
+
toDelete.push(row.id);
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
if (toDelete.length > 0) {
|
|
291
|
+
const deleteStmt = db.prepare('DELETE FROM lessons WHERE id = ?');
|
|
292
|
+
const deleteAll = db.transaction(() => {
|
|
293
|
+
for (const id of toDelete) {
|
|
294
|
+
deleteStmt.run(id);
|
|
295
|
+
}
|
|
296
|
+
});
|
|
297
|
+
deleteAll();
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
return { removed: toDelete.length, kept: all.length - toDelete.length };
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Full-text search across lessons using FTS5.
|
|
305
|
+
*
|
|
306
|
+
* @param {import('better-sqlite3').Database} db
|
|
307
|
+
* @param {string} query - Search text (FTS5 query syntax supported)
|
|
308
|
+
* @param {object} [options]
|
|
309
|
+
* @param {number} [options.limit=10]
|
|
310
|
+
* @param {string} [options.signal] - Filter by 'positive' or 'negative'
|
|
311
|
+
* @param {string[]} [options.tags] - Require ALL tags present
|
|
312
|
+
* @param {string} [options.domain] - Filter by domain
|
|
313
|
+
* @returns {Array<object>}
|
|
314
|
+
*/
|
|
315
|
+
function searchLessons(db, query, options = {}) {
|
|
316
|
+
const limit = Math.min(options.limit || 10, 50);
|
|
317
|
+
|
|
318
|
+
if (!query || !query.trim()) {
|
|
319
|
+
// No query — return most recent lessons with optional filters
|
|
320
|
+
let sql = 'SELECT * FROM lessons WHERE 1=1';
|
|
321
|
+
const params = [];
|
|
322
|
+
|
|
323
|
+
if (options.signal) {
|
|
324
|
+
sql += ' AND signal = ?';
|
|
325
|
+
params.push(options.signal);
|
|
326
|
+
}
|
|
327
|
+
if (options.domain) {
|
|
328
|
+
sql += ' AND domain = ?';
|
|
329
|
+
params.push(options.domain);
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
sql += ' ORDER BY timestamp DESC LIMIT ?';
|
|
333
|
+
params.push(limit);
|
|
334
|
+
|
|
335
|
+
const rows = db.prepare(sql).all(...params);
|
|
336
|
+
return rows.map(parseRow);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// FTS5 search with ranking
|
|
340
|
+
const safeQuery = sanitizeFtsQuery(query);
|
|
341
|
+
let sql = `
|
|
342
|
+
SELECT l.*, rank
|
|
343
|
+
FROM lessons_fts fts
|
|
344
|
+
JOIN lessons l ON l.rowid = fts.rowid
|
|
345
|
+
WHERE lessons_fts MATCH ?
|
|
346
|
+
`;
|
|
347
|
+
const params = [safeQuery];
|
|
348
|
+
|
|
349
|
+
if (options.signal) {
|
|
350
|
+
sql += ' AND l.signal = ?';
|
|
351
|
+
params.push(options.signal);
|
|
352
|
+
}
|
|
353
|
+
if (options.domain) {
|
|
354
|
+
sql += ' AND l.domain = ?';
|
|
355
|
+
params.push(options.domain);
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
sql += ' ORDER BY rank LIMIT ?';
|
|
359
|
+
params.push(limit);
|
|
360
|
+
|
|
361
|
+
let rows;
|
|
362
|
+
try {
|
|
363
|
+
rows = db.prepare(sql).all(...params);
|
|
364
|
+
} catch (_err) {
|
|
365
|
+
// If FTS query syntax is invalid, fall back to LIKE search
|
|
366
|
+
rows = fallbackLikeSearch(db, query, options, limit);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const parsed = rows.map(parseRow);
|
|
370
|
+
|
|
371
|
+
// Post-filter by tags (JSON array stored as text)
|
|
372
|
+
if (options.tags && options.tags.length > 0) {
|
|
373
|
+
return parsed.filter((row) => {
|
|
374
|
+
const rowTags = row.tags || [];
|
|
375
|
+
return options.tags.every((t) => rowTags.includes(t));
|
|
376
|
+
});
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
return parsed;
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Find corrective actions for a negative feedback event by matching
|
|
384
|
+
* similar past failures (by tags and domain).
|
|
385
|
+
*
|
|
386
|
+
* @param {import('better-sqlite3').Database} db
|
|
387
|
+
* @param {object} feedbackEvent
|
|
388
|
+
* @param {number} [limit=3]
|
|
389
|
+
* @returns {string[]} Top corrective actions
|
|
390
|
+
*/
|
|
391
|
+
function inferCorrectiveActions(db, feedbackEvent, limit = 3) {
|
|
392
|
+
const tags = Array.isArray(feedbackEvent.tags) ? feedbackEvent.tags : [];
|
|
393
|
+
const domain = feedbackEvent.richContext?.domain || 'general';
|
|
394
|
+
|
|
395
|
+
// Strategy: find past negative lessons with overlapping tags, ranked by recency
|
|
396
|
+
let candidates = [];
|
|
397
|
+
|
|
398
|
+
// 1. Tag overlap search — find lessons sharing tags with this failure
|
|
399
|
+
if (tags.length > 0) {
|
|
400
|
+
const tagPlaceholders = tags.map(() => `l.tags LIKE ?`).join(' OR ');
|
|
401
|
+
const tagParams = tags.map((t) => `%"${t}"%`);
|
|
402
|
+
|
|
403
|
+
const sql = `
|
|
404
|
+
SELECT whatToChange, tags, timestamp
|
|
405
|
+
FROM lessons l
|
|
406
|
+
WHERE l.signal = 'negative'
|
|
407
|
+
AND l.whatToChange IS NOT NULL
|
|
408
|
+
AND l.whatToChange != ''
|
|
409
|
+
AND (${tagPlaceholders})
|
|
410
|
+
ORDER BY l.timestamp DESC
|
|
411
|
+
LIMIT 20
|
|
412
|
+
`;
|
|
413
|
+
|
|
414
|
+
candidates = db.prepare(sql).all(...tagParams);
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// 2. Domain fallback if no tag matches
|
|
418
|
+
if (candidates.length === 0) {
|
|
419
|
+
const sql = `
|
|
420
|
+
SELECT whatToChange, tags, timestamp
|
|
421
|
+
FROM lessons l
|
|
422
|
+
WHERE l.signal = 'negative'
|
|
423
|
+
AND l.whatToChange IS NOT NULL
|
|
424
|
+
AND l.whatToChange != ''
|
|
425
|
+
AND l.domain = ?
|
|
426
|
+
ORDER BY l.timestamp DESC
|
|
427
|
+
LIMIT 10
|
|
428
|
+
`;
|
|
429
|
+
|
|
430
|
+
candidates = db.prepare(sql).all(domain);
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (candidates.length === 0) return [];
|
|
434
|
+
|
|
435
|
+
// Deduplicate and rank by tag overlap count
|
|
436
|
+
const actionMap = new Map();
|
|
437
|
+
for (const c of candidates) {
|
|
438
|
+
const action = c.whatToChange.trim();
|
|
439
|
+
if (actionMap.has(action)) {
|
|
440
|
+
actionMap.get(action).count += 1;
|
|
441
|
+
} else {
|
|
442
|
+
const cTags = safeParseTags(c.tags);
|
|
443
|
+
const overlap = tags.filter((t) => cTags.includes(t)).length;
|
|
444
|
+
actionMap.set(action, { action, count: 1, overlap, timestamp: c.timestamp });
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
return Array.from(actionMap.values())
|
|
449
|
+
.sort((a, b) => b.overlap - a.overlap || b.count - a.count)
|
|
450
|
+
.slice(0, limit)
|
|
451
|
+
.map((a) => a.action);
|
|
452
|
+
}
|
|
453
|
+
|
|
454
|
+
/**
|
|
455
|
+
* Get lesson count and signal breakdown.
|
|
456
|
+
*/
|
|
457
|
+
function getStats(db) {
|
|
458
|
+
const total = db.prepare('SELECT COUNT(*) as count FROM lessons').get();
|
|
459
|
+
const bySignal = db.prepare('SELECT signal, COUNT(*) as count FROM lessons GROUP BY signal').all();
|
|
460
|
+
const byDomain = db.prepare('SELECT domain, COUNT(*) as count FROM lessons GROUP BY domain ORDER BY count DESC LIMIT 10').all();
|
|
461
|
+
return { total: total.count, bySignal, byDomain };
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Fast stats computation from SQLite — replaces slow JSONL scan.
|
|
466
|
+
* Used by feedback_stats MCP tool to prevent 300s timeouts.
|
|
467
|
+
*/
|
|
468
|
+
function getStatsFromDB(db) {
|
|
469
|
+
const total = db.prepare('SELECT COUNT(*) as count FROM lessons').get().count;
|
|
470
|
+
const positive = db.prepare("SELECT COUNT(*) as count FROM lessons WHERE signal = 'positive'").get().count;
|
|
471
|
+
const negative = db.prepare("SELECT COUNT(*) as count FROM lessons WHERE signal = 'negative'").get().count;
|
|
472
|
+
const byDomain = db.prepare('SELECT domain, COUNT(*) as count FROM lessons GROUP BY domain ORDER BY count DESC').all();
|
|
473
|
+
const byImportance = db.prepare('SELECT importance, COUNT(*) as count FROM lessons GROUP BY importance ORDER BY count DESC').all();
|
|
474
|
+
const recentLessons = db.prepare('SELECT id, signal, context, domain, timestamp FROM lessons ORDER BY timestamp DESC LIMIT 10').all();
|
|
475
|
+
const sessionCount = db.prepare('SELECT COUNT(*) as count FROM sessions').get().count;
|
|
476
|
+
|
|
477
|
+
return {
|
|
478
|
+
source: 'sqlite',
|
|
479
|
+
total,
|
|
480
|
+
positive,
|
|
481
|
+
negative,
|
|
482
|
+
positiveRate: total > 0 ? Math.round((positive / total) * 100) : 0,
|
|
483
|
+
byDomain,
|
|
484
|
+
byImportance,
|
|
485
|
+
recentLessons,
|
|
486
|
+
sessionCount,
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
/**
|
|
491
|
+
* Backfill SQLite from existing JSONL files.
|
|
492
|
+
* Reads feedback-log.jsonl and memory-log.jsonl, upserts all records.
|
|
493
|
+
*/
|
|
494
|
+
function backfillFromJsonl(db, feedbackDir) {
|
|
495
|
+
const feedbackLogPath = path.join(feedbackDir, 'feedback-log.jsonl');
|
|
496
|
+
const memoryLogPath = path.join(feedbackDir, 'memory-log.jsonl');
|
|
497
|
+
|
|
498
|
+
const feedbackEntries = readJsonlSafe(feedbackLogPath);
|
|
499
|
+
const memoryEntries = readJsonlSafe(memoryLogPath);
|
|
500
|
+
|
|
501
|
+
// Index memories by sourceFeedbackId for joining
|
|
502
|
+
const memoryByFeedbackId = new Map();
|
|
503
|
+
for (const m of memoryEntries) {
|
|
504
|
+
if (m.sourceFeedbackId) {
|
|
505
|
+
memoryByFeedbackId.set(m.sourceFeedbackId, m);
|
|
506
|
+
}
|
|
507
|
+
}
|
|
508
|
+
|
|
509
|
+
const insert = db.transaction(() => {
|
|
510
|
+
let count = 0;
|
|
511
|
+
for (const fb of feedbackEntries) {
|
|
512
|
+
if (!fb.id || !fb.signal) continue;
|
|
513
|
+
const mem = memoryByFeedbackId.get(fb.id) || null;
|
|
514
|
+
upsertLesson(db, fb, mem);
|
|
515
|
+
count++;
|
|
516
|
+
}
|
|
517
|
+
return count;
|
|
518
|
+
});
|
|
519
|
+
|
|
520
|
+
return insert();
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ---------------------------------------------------------------------------
|
|
524
|
+
// Internal helpers
|
|
525
|
+
// ---------------------------------------------------------------------------
|
|
526
|
+
|
|
527
|
+
function sanitizeFtsQuery(query) {
|
|
528
|
+
// Escape FTS5 special chars, convert to prefix search terms
|
|
529
|
+
return query
|
|
530
|
+
.replace(/[":*()^~]/g, ' ')
|
|
531
|
+
.trim()
|
|
532
|
+
.split(/\s+/)
|
|
533
|
+
.filter(Boolean)
|
|
534
|
+
.map((term) => `"${term}"`)
|
|
535
|
+
.join(' ');
|
|
536
|
+
}
|
|
537
|
+
|
|
538
|
+
function fallbackLikeSearch(db, query, options, limit) {
|
|
539
|
+
const terms = query.split(/\s+/).filter(Boolean);
|
|
540
|
+
if (terms.length === 0) return [];
|
|
541
|
+
|
|
542
|
+
const conditions = terms.map(
|
|
543
|
+
() => `(context LIKE ? OR whatWentWrong LIKE ? OR whatToChange LIKE ? OR whatWorked LIKE ?)`,
|
|
544
|
+
);
|
|
545
|
+
const params = terms.flatMap((t) => {
|
|
546
|
+
const like = `%${t}%`;
|
|
547
|
+
return [like, like, like, like];
|
|
548
|
+
});
|
|
549
|
+
|
|
550
|
+
let sql = `SELECT * FROM lessons WHERE ${conditions.join(' AND ')}`;
|
|
551
|
+
|
|
552
|
+
if (options.signal) {
|
|
553
|
+
sql += ' AND signal = ?';
|
|
554
|
+
params.push(options.signal);
|
|
555
|
+
}
|
|
556
|
+
if (options.domain) {
|
|
557
|
+
sql += ' AND domain = ?';
|
|
558
|
+
params.push(options.domain);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
sql += ' ORDER BY timestamp DESC LIMIT ?';
|
|
562
|
+
params.push(limit);
|
|
563
|
+
|
|
564
|
+
return db.prepare(sql).all(...params);
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
function parseRow(row) {
|
|
568
|
+
return {
|
|
569
|
+
...row,
|
|
570
|
+
tags: safeParseTags(row.tags),
|
|
571
|
+
pruned: row.pruned === 1,
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
function safeParseTags(tagsStr) {
|
|
576
|
+
try {
|
|
577
|
+
const parsed = JSON.parse(tagsStr || '[]');
|
|
578
|
+
return Array.isArray(parsed) ? parsed : [];
|
|
579
|
+
} catch {
|
|
580
|
+
return [];
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
function readJsonlSafe(filePath) {
|
|
585
|
+
if (!fs.existsSync(filePath)) return [];
|
|
586
|
+
const raw = fs.readFileSync(filePath, 'utf-8').trim();
|
|
587
|
+
if (!raw) return [];
|
|
588
|
+
return raw
|
|
589
|
+
.split('\n')
|
|
590
|
+
.map((line) => {
|
|
591
|
+
try {
|
|
592
|
+
return JSON.parse(line);
|
|
593
|
+
} catch {
|
|
594
|
+
return null;
|
|
595
|
+
}
|
|
596
|
+
})
|
|
597
|
+
.filter(Boolean);
|
|
598
|
+
}
|
|
599
|
+
|
|
600
|
+
module.exports = {
|
|
601
|
+
initDB,
|
|
602
|
+
upsertLesson,
|
|
603
|
+
upsertSession,
|
|
604
|
+
searchSessions,
|
|
605
|
+
findDuplicate,
|
|
606
|
+
compactLessons,
|
|
607
|
+
searchLessons,
|
|
608
|
+
inferCorrectiveActions,
|
|
609
|
+
getStats,
|
|
610
|
+
getStatsFromDB,
|
|
611
|
+
backfillFromJsonl,
|
|
612
|
+
DEFAULT_DB_PATH,
|
|
613
|
+
};
|