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,301 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* GPT-5.4 Tier Router — routes tasks to nano/mini/frontier based on
|
|
6
|
+
* task complexity, context size, risk level, and retry count.
|
|
7
|
+
* Includes frontier budget control.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
const path = require('path');
|
|
11
|
+
const { recommendInferenceBackend } = require('./local-model-profile');
|
|
12
|
+
|
|
13
|
+
const CONFIG_PATH = path.join(__dirname, '..', 'config', 'model-tiers.json');
|
|
14
|
+
|
|
15
|
+
// ---------------------------------------------------------------------------
|
|
16
|
+
// Load config
|
|
17
|
+
// ---------------------------------------------------------------------------
|
|
18
|
+
|
|
19
|
+
let _config;
|
|
20
|
+
function loadConfig() {
|
|
21
|
+
if (!_config) _config = require(CONFIG_PATH);
|
|
22
|
+
return _config;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
// Model tiers
|
|
27
|
+
// ---------------------------------------------------------------------------
|
|
28
|
+
|
|
29
|
+
const TIERS = {
|
|
30
|
+
nano: { label: 'nano', costMultiplier: 0.1, maxContext: 32000 },
|
|
31
|
+
mini: { label: 'mini', costMultiplier: 0.4, maxContext: 200000 },
|
|
32
|
+
frontier: { label: 'frontier', costMultiplier: 1.0, maxContext: 1000000 },
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
// ---------------------------------------------------------------------------
|
|
36
|
+
// Task classification → tier mapping
|
|
37
|
+
// ---------------------------------------------------------------------------
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Classify a task and route it to the appropriate model tier.
|
|
41
|
+
*
|
|
42
|
+
* @param {object} task
|
|
43
|
+
* @param {string} task.type — task type identifier
|
|
44
|
+
* @param {number} [task.contextTokens] — estimated context window usage
|
|
45
|
+
* @param {string} [task.riskLevel] — 'low' | 'medium' | 'high'
|
|
46
|
+
* @param {number} [task.retryCount] — how many times this task has been retried
|
|
47
|
+
* @param {string[]} [task.tags] — freeform tags for classification
|
|
48
|
+
* @returns {{ tier: string, reason: string, escalated: boolean }}
|
|
49
|
+
*/
|
|
50
|
+
function classifyTask(task = {}) {
|
|
51
|
+
const { type, contextTokens = 0, riskLevel, retryCount = 0, tags = [] } = task;
|
|
52
|
+
const config = loadConfig();
|
|
53
|
+
const escalation = config.escalationRules;
|
|
54
|
+
const archTags = escalation.architectureTags || [];
|
|
55
|
+
|
|
56
|
+
// --- Escalation checks (override normal routing) ---
|
|
57
|
+
|
|
58
|
+
// 1. Context exceeds frontier threshold
|
|
59
|
+
if (contextTokens > escalation.contextThreshold) {
|
|
60
|
+
return {
|
|
61
|
+
tier: 'frontier',
|
|
62
|
+
reason: `context size ${contextTokens} exceeds threshold ${escalation.contextThreshold}`,
|
|
63
|
+
escalated: true,
|
|
64
|
+
};
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
// 2. High risk + retried enough
|
|
68
|
+
if (riskLevel === 'high' && retryCount >= escalation.failureRetryThreshold) {
|
|
69
|
+
return {
|
|
70
|
+
tier: 'frontier',
|
|
71
|
+
reason: `high risk with ${retryCount} retries (threshold: ${escalation.failureRetryThreshold})`,
|
|
72
|
+
escalated: true,
|
|
73
|
+
};
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// 3. Architecture / cross-file tags
|
|
77
|
+
const matchedTag = tags.find((t) => archTags.includes(t));
|
|
78
|
+
if (matchedTag) {
|
|
79
|
+
return {
|
|
80
|
+
tier: 'frontier',
|
|
81
|
+
reason: `tag "${matchedTag}" matches architecture escalation`,
|
|
82
|
+
escalated: true,
|
|
83
|
+
};
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// --- Normal tier routing by task type ---
|
|
87
|
+
|
|
88
|
+
const tiers = config.tiers;
|
|
89
|
+
for (const tierName of ['nano', 'mini', 'frontier']) {
|
|
90
|
+
if (tiers[tierName].taskTypes.includes(type)) {
|
|
91
|
+
return {
|
|
92
|
+
tier: tierName,
|
|
93
|
+
reason: `task type "${type}" mapped to ${tierName}`,
|
|
94
|
+
escalated: false,
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// Unknown type defaults to mini
|
|
100
|
+
return {
|
|
101
|
+
tier: 'mini',
|
|
102
|
+
reason: `unknown task type "${type}" — defaulting to mini`,
|
|
103
|
+
escalated: false,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ---------------------------------------------------------------------------
|
|
108
|
+
// Escalation logic
|
|
109
|
+
// ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* Determine whether a task should be escalated from its current tier.
|
|
113
|
+
*
|
|
114
|
+
* @param {object} task — same shape as classifyTask input
|
|
115
|
+
* @param {object[]} history — array of { tier, success } from previous attempts
|
|
116
|
+
* @returns {{ escalate: boolean, from: string, to: string, reason: string }}
|
|
117
|
+
*/
|
|
118
|
+
function shouldEscalate(task = {}, history = []) {
|
|
119
|
+
const { contextTokens = 0, riskLevel, retryCount = 0, tags = [] } = task;
|
|
120
|
+
const config = loadConfig();
|
|
121
|
+
const rules = config.escalationRules;
|
|
122
|
+
const archTags = rules.architectureTags || [];
|
|
123
|
+
|
|
124
|
+
const currentTier = classifyTask(task).tier;
|
|
125
|
+
|
|
126
|
+
// 1. Context exceeds threshold
|
|
127
|
+
if (contextTokens > rules.contextThreshold && currentTier !== 'frontier') {
|
|
128
|
+
return {
|
|
129
|
+
escalate: true,
|
|
130
|
+
from: currentTier,
|
|
131
|
+
to: 'frontier',
|
|
132
|
+
reason: `context ${contextTokens} > threshold ${rules.contextThreshold}`,
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
// 2. High risk + retries
|
|
137
|
+
if (riskLevel === 'high' && retryCount >= rules.failureRetryThreshold && currentTier !== 'frontier') {
|
|
138
|
+
return {
|
|
139
|
+
escalate: true,
|
|
140
|
+
from: currentTier,
|
|
141
|
+
to: 'frontier',
|
|
142
|
+
reason: `high risk with ${retryCount} retries`,
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// 3. Architecture tags
|
|
147
|
+
const matchedTag = tags.find((t) => archTags.includes(t));
|
|
148
|
+
if (matchedTag && currentTier !== 'frontier') {
|
|
149
|
+
return {
|
|
150
|
+
escalate: true,
|
|
151
|
+
from: currentTier,
|
|
152
|
+
to: 'frontier',
|
|
153
|
+
reason: `architecture tag "${matchedTag}"`,
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
// 4. Two consecutive failures at mini tier
|
|
158
|
+
if (history.length >= 2) {
|
|
159
|
+
const lastTwo = history.slice(-2);
|
|
160
|
+
if (lastTwo.every((h) => h.tier === 'mini' && !h.success)) {
|
|
161
|
+
return {
|
|
162
|
+
escalate: true,
|
|
163
|
+
from: 'mini',
|
|
164
|
+
to: 'frontier',
|
|
165
|
+
reason: 'two consecutive failures at mini tier',
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
escalate: false,
|
|
172
|
+
from: currentTier,
|
|
173
|
+
to: currentTier,
|
|
174
|
+
reason: 'no escalation needed',
|
|
175
|
+
};
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// ---------------------------------------------------------------------------
|
|
179
|
+
// Frontier budget tracker
|
|
180
|
+
// ---------------------------------------------------------------------------
|
|
181
|
+
|
|
182
|
+
class FrontierBudget {
|
|
183
|
+
/**
|
|
184
|
+
* @param {object} [options]
|
|
185
|
+
* @param {number} [options.tokenCap] — max frontier tokens per session (default 500000)
|
|
186
|
+
* @param {boolean} [options.requireReason] — require a reason string for spend (default true)
|
|
187
|
+
*/
|
|
188
|
+
constructor(options = {}) {
|
|
189
|
+
const config = loadConfig();
|
|
190
|
+
const defaults = config.tiers.frontier.budgetDefaults || {};
|
|
191
|
+
this.tokenCap = options.tokenCap ?? defaults.tokenCap ?? 500000;
|
|
192
|
+
this.requireReason = options.requireReason ?? defaults.requireReason ?? true;
|
|
193
|
+
this.spent = 0;
|
|
194
|
+
this.invocations = [];
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Check whether a spend is allowed without deducting.
|
|
199
|
+
* @param {number} tokens
|
|
200
|
+
* @param {string} [reason]
|
|
201
|
+
* @returns {{ allowed: boolean, remaining: number, reason: string }}
|
|
202
|
+
*/
|
|
203
|
+
canSpend(tokens, reason) {
|
|
204
|
+
if (this.requireReason && !reason) {
|
|
205
|
+
return {
|
|
206
|
+
allowed: false,
|
|
207
|
+
remaining: this.tokenCap - this.spent,
|
|
208
|
+
reason: 'reason is required for frontier spend',
|
|
209
|
+
};
|
|
210
|
+
}
|
|
211
|
+
const remaining = this.tokenCap - this.spent;
|
|
212
|
+
if (tokens > remaining) {
|
|
213
|
+
return {
|
|
214
|
+
allowed: false,
|
|
215
|
+
remaining,
|
|
216
|
+
reason: `requested ${tokens} exceeds remaining ${remaining}`,
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
return {
|
|
220
|
+
allowed: true,
|
|
221
|
+
remaining,
|
|
222
|
+
reason: 'within budget',
|
|
223
|
+
};
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Deduct tokens from the budget.
|
|
228
|
+
* @param {number} tokens
|
|
229
|
+
* @param {string} [reason]
|
|
230
|
+
* @returns {{ success: boolean, spent: number, remaining: number, reason: string }}
|
|
231
|
+
*/
|
|
232
|
+
spend(tokens, reason) {
|
|
233
|
+
const check = this.canSpend(tokens, reason);
|
|
234
|
+
if (!check.allowed) {
|
|
235
|
+
return { success: false, spent: this.spent, remaining: check.remaining, reason: check.reason };
|
|
236
|
+
}
|
|
237
|
+
this.spent += tokens;
|
|
238
|
+
this.invocations.push({ tokens, reason, timestamp: new Date().toISOString() });
|
|
239
|
+
return {
|
|
240
|
+
success: true,
|
|
241
|
+
spent: this.spent,
|
|
242
|
+
remaining: this.tokenCap - this.spent,
|
|
243
|
+
reason: `spent ${tokens} tokens — ${reason}`,
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
/**
|
|
248
|
+
* Return current budget status.
|
|
249
|
+
* @returns {{ spent: number, remaining: number, cap: number, invocations: number }}
|
|
250
|
+
*/
|
|
251
|
+
status() {
|
|
252
|
+
return {
|
|
253
|
+
spent: this.spent,
|
|
254
|
+
remaining: this.tokenCap - this.spent,
|
|
255
|
+
cap: this.tokenCap,
|
|
256
|
+
invocations: this.invocations.length,
|
|
257
|
+
};
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
/** Reset the budget for a new session. */
|
|
261
|
+
reset() {
|
|
262
|
+
this.spent = 0;
|
|
263
|
+
this.invocations = [];
|
|
264
|
+
}
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
function recommendExecutionPlan(task = {}, env = process.env) {
|
|
268
|
+
const classification = classifyTask(task);
|
|
269
|
+
const inference = recommendInferenceBackend(task, env);
|
|
270
|
+
|
|
271
|
+
return {
|
|
272
|
+
tier: classification.tier,
|
|
273
|
+
escalated: classification.escalated,
|
|
274
|
+
tierReason: classification.reason,
|
|
275
|
+
backendId: inference.backend.id,
|
|
276
|
+
providerMode: inference.backend.providerMode,
|
|
277
|
+
workloadClass: inference.workloadClass,
|
|
278
|
+
recommendationClass: inference.recommendationClass,
|
|
279
|
+
indexCacheEligible: inference.backend.indexCacheEligible,
|
|
280
|
+
indexCacheEnabled: inference.backend.indexCacheEnabled,
|
|
281
|
+
reason: `${classification.reason}; ${inference.reason}`,
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
// ---------------------------------------------------------------------------
|
|
286
|
+
// Exports
|
|
287
|
+
// ---------------------------------------------------------------------------
|
|
288
|
+
|
|
289
|
+
module.exports = { TIERS, classifyTask, shouldEscalate, FrontierBudget, recommendExecutionPlan };
|
|
290
|
+
|
|
291
|
+
// ---------------------------------------------------------------------------
|
|
292
|
+
// CLI
|
|
293
|
+
// ---------------------------------------------------------------------------
|
|
294
|
+
|
|
295
|
+
if (require.main === module) {
|
|
296
|
+
const taskType = process.argv[2] || 'code-edit';
|
|
297
|
+
const result = classifyTask({ type: taskType });
|
|
298
|
+
const execution = recommendExecutionPlan({ type: taskType });
|
|
299
|
+
const budget = new FrontierBudget();
|
|
300
|
+
console.log(JSON.stringify({ classification: result, execution, budget: budget.status() }, null, 2));
|
|
301
|
+
}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
/**
|
|
3
|
+
* money-watcher.js
|
|
4
|
+
* Continuously polls the commercial summary for net-new paid orders or booked revenue.
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
'use strict';
|
|
8
|
+
|
|
9
|
+
const { getOperationalBillingSummary } = require('./operational-summary');
|
|
10
|
+
|
|
11
|
+
function getCommercialRevenueSnapshot(summary) {
|
|
12
|
+
const revenue = summary.revenue || {};
|
|
13
|
+
return {
|
|
14
|
+
paidOrders: revenue.paidOrders || 0,
|
|
15
|
+
bookedRevenueCents: revenue.bookedRevenueCents || 0,
|
|
16
|
+
latestPaidAt: revenue.latestPaidAt || null,
|
|
17
|
+
latestPaidOrder: revenue.latestPaidOrder || null,
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async function watchMoney(intervalMs = 10000) {
|
|
22
|
+
console.log('👀 Money Watcher activated. Polling billing summary for commercial changes...');
|
|
23
|
+
const initialState = await getOperationalBillingSummary();
|
|
24
|
+
let initialSnapshot = getCommercialRevenueSnapshot(initialState.summary);
|
|
25
|
+
let polling = false;
|
|
26
|
+
|
|
27
|
+
return setInterval(async () => {
|
|
28
|
+
if (polling) return;
|
|
29
|
+
polling = true;
|
|
30
|
+
try {
|
|
31
|
+
const { source, summary, fallbackReason } = await getOperationalBillingSummary();
|
|
32
|
+
const currentSnapshot = getCommercialRevenueSnapshot(summary);
|
|
33
|
+
|
|
34
|
+
const newPaidOrders = currentSnapshot.paidOrders - initialSnapshot.paidOrders;
|
|
35
|
+
const newBookedRevenue = currentSnapshot.bookedRevenueCents - initialSnapshot.bookedRevenueCents;
|
|
36
|
+
|
|
37
|
+
if (newPaidOrders > 0 || newBookedRevenue > 0) {
|
|
38
|
+
console.log('\n🚨🚨🚨 COMMERCIAL ALERT: NET-NEW PAID ACTIVITY DETECTED! 🚨🚨🚨');
|
|
39
|
+
console.log('Operational billing summary:');
|
|
40
|
+
console.log(JSON.stringify({
|
|
41
|
+
source,
|
|
42
|
+
fallbackReason,
|
|
43
|
+
newPaidOrders,
|
|
44
|
+
newBookedRevenueCents: newBookedRevenue,
|
|
45
|
+
latestPaidAt: currentSnapshot.latestPaidAt,
|
|
46
|
+
latestPaidOrder: currentSnapshot.latestPaidOrder,
|
|
47
|
+
bookedRevenueCents: currentSnapshot.bookedRevenueCents,
|
|
48
|
+
activeKeys: summary.keys.active,
|
|
49
|
+
totalUsage: summary.keys.totalUsage,
|
|
50
|
+
}, null, 2));
|
|
51
|
+
|
|
52
|
+
process.stdout.write('\x07');
|
|
53
|
+
initialSnapshot = currentSnapshot;
|
|
54
|
+
}
|
|
55
|
+
} finally {
|
|
56
|
+
polling = false;
|
|
57
|
+
}
|
|
58
|
+
}, intervalMs);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
if (require.main === module) {
|
|
62
|
+
watchMoney().catch((err) => {
|
|
63
|
+
console.error(err && err.message ? err.message : err);
|
|
64
|
+
process.exit(1);
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
module.exports = {
|
|
69
|
+
getCommercialRevenueSnapshot,
|
|
70
|
+
watchMoney,
|
|
71
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Multi-Hop Recall — chains related lessons across hops for deeper context.
|
|
6
|
+
*
|
|
7
|
+
* Single-hop: query → top N lessons (current behavior)
|
|
8
|
+
* Multi-hop: query → hop-1 lessons → extract tags/domains/rootCauses →
|
|
9
|
+
* hop-2 lessons (related) → deduplicate → ranked chain
|
|
10
|
+
*
|
|
11
|
+
* Inspired by Chroma Context-1's multi-hop retrieval pattern.
|
|
12
|
+
* Pro-only feature — gated via requirePro('multi-hop-recall').
|
|
13
|
+
*
|
|
14
|
+
* @module multi-hop-recall
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const { requirePro } = require('./pro-features');
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Extract expansion terms from a set of lessons for the next hop.
|
|
21
|
+
* Pulls tags, domains, rootCauses, and key phrases from whatToChange.
|
|
22
|
+
*/
|
|
23
|
+
function extractExpansionTerms(lessons) {
|
|
24
|
+
const terms = new Set();
|
|
25
|
+
|
|
26
|
+
for (const lesson of lessons) {
|
|
27
|
+
// Tags
|
|
28
|
+
const tags = Array.isArray(lesson.tags) ? lesson.tags : safeParseTags(lesson.tags);
|
|
29
|
+
for (const tag of tags) {
|
|
30
|
+
if (tag && tag.length > 2) terms.add(tag);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// Domain
|
|
34
|
+
if (lesson.domain && lesson.domain !== 'general') {
|
|
35
|
+
terms.add(lesson.domain);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Root cause category
|
|
39
|
+
if (lesson.rootCause) {
|
|
40
|
+
terms.add(lesson.rootCause);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// Key phrases from whatToChange (3+ char words, skip stopwords)
|
|
44
|
+
if (lesson.whatToChange) {
|
|
45
|
+
const words = lesson.whatToChange
|
|
46
|
+
.toLowerCase()
|
|
47
|
+
.replace(/[^a-z0-9\s]/g, '')
|
|
48
|
+
.split(/\s+/)
|
|
49
|
+
.filter((w) => w.length > 3 && !STOPWORDS.has(w));
|
|
50
|
+
for (const word of words.slice(0, 5)) {
|
|
51
|
+
terms.add(word);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
return Array.from(terms);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const STOPWORDS = new Set([
|
|
60
|
+
'this', 'that', 'with', 'from', 'have', 'been', 'were', 'will',
|
|
61
|
+
'should', 'would', 'could', 'about', 'their', 'there', 'which',
|
|
62
|
+
'when', 'what', 'than', 'then', 'them', 'they', 'into', 'some',
|
|
63
|
+
'also', 'more', 'very', 'just', 'does', 'done', 'make', 'made',
|
|
64
|
+
]);
|
|
65
|
+
|
|
66
|
+
function safeParseTags(tags) {
|
|
67
|
+
if (Array.isArray(tags)) return tags;
|
|
68
|
+
if (typeof tags === 'string') {
|
|
69
|
+
try { return JSON.parse(tags); } catch { return []; }
|
|
70
|
+
}
|
|
71
|
+
return [];
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Deduplicate lessons by ID, keeping first occurrence (higher-ranked).
|
|
76
|
+
*/
|
|
77
|
+
function deduplicateById(lessons) {
|
|
78
|
+
const seen = new Set();
|
|
79
|
+
return lessons.filter((l) => {
|
|
80
|
+
const id = l.id;
|
|
81
|
+
if (seen.has(id)) return false;
|
|
82
|
+
seen.add(id);
|
|
83
|
+
return true;
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Score a lesson's relevance to a set of expansion terms.
|
|
89
|
+
* Higher = more relevant to the chain.
|
|
90
|
+
*/
|
|
91
|
+
function scoreRelevance(lesson, expansionTerms) {
|
|
92
|
+
let score = 0;
|
|
93
|
+
const termSet = new Set(expansionTerms.map((t) => t.toLowerCase()));
|
|
94
|
+
|
|
95
|
+
// Tag overlap: 3 points per matching tag
|
|
96
|
+
const tags = Array.isArray(lesson.tags) ? lesson.tags : safeParseTags(lesson.tags);
|
|
97
|
+
for (const tag of tags) {
|
|
98
|
+
if (termSet.has(tag.toLowerCase())) score += 3;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Domain match: 2 points
|
|
102
|
+
if (lesson.domain && termSet.has(lesson.domain.toLowerCase())) score += 2;
|
|
103
|
+
|
|
104
|
+
// Root cause match: 2 points
|
|
105
|
+
if (lesson.rootCause && termSet.has(lesson.rootCause.toLowerCase())) score += 2;
|
|
106
|
+
|
|
107
|
+
// Content overlap: 1 point per matching word in whatToChange
|
|
108
|
+
if (lesson.whatToChange) {
|
|
109
|
+
const words = lesson.whatToChange.toLowerCase().split(/\s+/);
|
|
110
|
+
for (const word of words) {
|
|
111
|
+
if (word.length > 3 && termSet.has(word)) score += 1;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return score;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Perform multi-hop recall over a lesson database.
|
|
120
|
+
*
|
|
121
|
+
* @param {Function} searchFn - Search function: (query, options) => lesson[]
|
|
122
|
+
* Must accept (query, { limit, signal, domain }) and return lesson rows.
|
|
123
|
+
* @param {string} query - Initial search query
|
|
124
|
+
* @param {object} [options]
|
|
125
|
+
* @param {number} [options.maxHops=2] - Maximum chain depth (1 = single-hop, 2+ = multi-hop)
|
|
126
|
+
* @param {number} [options.hopLimit=10] - Max results per hop
|
|
127
|
+
* @param {number} [options.totalLimit=15] - Max total results across all hops
|
|
128
|
+
* @param {string} [options.signal] - Filter by 'positive' or 'negative'
|
|
129
|
+
* @param {boolean} [options.skipProCheck=false] - Skip Pro license check (for testing)
|
|
130
|
+
* @param {Function} [options.requireProFn=requirePro] - Injectable Pro gate helper for testing
|
|
131
|
+
* @returns {{ results: object[], hops: object[], totalHops: number, expansionTerms: string[] }}
|
|
132
|
+
*/
|
|
133
|
+
function multiHopRecall(searchFn, query, options = {}) {
|
|
134
|
+
const {
|
|
135
|
+
maxHops = 2,
|
|
136
|
+
hopLimit = 10,
|
|
137
|
+
totalLimit = 15,
|
|
138
|
+
signal,
|
|
139
|
+
skipProCheck = false,
|
|
140
|
+
requireProFn = requirePro,
|
|
141
|
+
} = options;
|
|
142
|
+
|
|
143
|
+
// Pro gate (unless testing)
|
|
144
|
+
if (!skipProCheck && !requireProFn('multi-hop-recall')) {
|
|
145
|
+
return { results: [], hops: [], totalHops: 0, expansionTerms: [], proRequired: true };
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
// Clamp hops to [1, 3] to prevent runaway chains
|
|
149
|
+
const hops = Math.max(1, Math.min(maxHops, 3));
|
|
150
|
+
const allResults = [];
|
|
151
|
+
const hopMetadata = [];
|
|
152
|
+
let expansionTerms = [];
|
|
153
|
+
|
|
154
|
+
// Hop 1: direct query search
|
|
155
|
+
const hop1Results = searchFn(query, { limit: hopLimit, signal });
|
|
156
|
+
allResults.push(...hop1Results);
|
|
157
|
+
hopMetadata.push({
|
|
158
|
+
hop: 1,
|
|
159
|
+
query,
|
|
160
|
+
resultsCount: hop1Results.length,
|
|
161
|
+
type: 'direct',
|
|
162
|
+
});
|
|
163
|
+
|
|
164
|
+
if (hops < 2 || hop1Results.length === 0) {
|
|
165
|
+
return {
|
|
166
|
+
results: deduplicateById(allResults).slice(0, totalLimit),
|
|
167
|
+
hops: hopMetadata,
|
|
168
|
+
totalHops: 1,
|
|
169
|
+
expansionTerms: [],
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
// Extract expansion terms from hop 1
|
|
174
|
+
expansionTerms = extractExpansionTerms(hop1Results);
|
|
175
|
+
|
|
176
|
+
if (expansionTerms.length === 0) {
|
|
177
|
+
return {
|
|
178
|
+
results: deduplicateById(allResults).slice(0, totalLimit),
|
|
179
|
+
hops: hopMetadata,
|
|
180
|
+
totalHops: 1,
|
|
181
|
+
expansionTerms: [],
|
|
182
|
+
};
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Hop 2+: search using expansion terms
|
|
186
|
+
for (let hop = 2; hop <= hops; hop++) {
|
|
187
|
+
// Build expansion query from top terms (limit to 5 to keep FTS manageable)
|
|
188
|
+
const queryTerms = expansionTerms.slice(0, 5).join(' OR ');
|
|
189
|
+
const hopResults = searchFn(queryTerms, { limit: hopLimit, signal });
|
|
190
|
+
|
|
191
|
+
// Score and sort by relevance to expansion terms
|
|
192
|
+
const scored = hopResults
|
|
193
|
+
.map((lesson) => ({ ...lesson, _hopScore: scoreRelevance(lesson, expansionTerms) }))
|
|
194
|
+
.filter((l) => l._hopScore > 0)
|
|
195
|
+
.sort((a, b) => b._hopScore - a._hopScore);
|
|
196
|
+
|
|
197
|
+
allResults.push(...scored);
|
|
198
|
+
hopMetadata.push({
|
|
199
|
+
hop,
|
|
200
|
+
query: queryTerms,
|
|
201
|
+
resultsCount: scored.length,
|
|
202
|
+
type: 'expansion',
|
|
203
|
+
termsUsed: expansionTerms.slice(0, 5),
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Extract new terms for next hop (if any)
|
|
207
|
+
if (hop < hops && scored.length > 0) {
|
|
208
|
+
const newTerms = extractExpansionTerms(scored);
|
|
209
|
+
// Only add truly new terms
|
|
210
|
+
const existingSet = new Set(expansionTerms);
|
|
211
|
+
const novel = newTerms.filter((t) => !existingSet.has(t));
|
|
212
|
+
expansionTerms = [...expansionTerms, ...novel];
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Deduplicate and cap at totalLimit
|
|
217
|
+
const deduplicated = deduplicateById(allResults).slice(0, totalLimit);
|
|
218
|
+
|
|
219
|
+
// Tag each result with its hop number
|
|
220
|
+
const hop1Ids = new Set(hop1Results.map((l) => l.id));
|
|
221
|
+
const tagged = deduplicated.map((l) => ({
|
|
222
|
+
...l,
|
|
223
|
+
_hop: hop1Ids.has(l.id) ? 1 : 2,
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
results: tagged,
|
|
228
|
+
hops: hopMetadata,
|
|
229
|
+
totalHops: hopMetadata.length,
|
|
230
|
+
expansionTerms,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
multiHopRecall,
|
|
236
|
+
extractExpansionTerms,
|
|
237
|
+
scoreRelevance,
|
|
238
|
+
deduplicateById,
|
|
239
|
+
STOPWORDS,
|
|
240
|
+
};
|