feed-the-machine 1.0.0 → 1.2.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.
- package/bin/generate-manifest.mjs +253 -0
- package/bin/install.mjs +134 -4
- package/docs/HOOKS.md +243 -0
- package/docs/INBOX.md +233 -0
- package/ftm/SKILL.md +34 -0
- package/ftm-audit/SKILL.md +69 -0
- package/ftm-brainstorm/SKILL.md +51 -0
- package/ftm-browse/SKILL.md +39 -0
- package/ftm-capture/SKILL.md +370 -0
- package/ftm-capture.yml +4 -0
- package/ftm-codex-gate/SKILL.md +59 -0
- package/ftm-config/SKILL.md +35 -0
- package/ftm-council/SKILL.md +56 -0
- package/ftm-dashboard/SKILL.md +163 -0
- package/ftm-debug/SKILL.md +84 -0
- package/ftm-diagram/SKILL.md +44 -0
- package/ftm-executor/SKILL.md +97 -0
- package/ftm-git/SKILL.md +60 -0
- package/ftm-inbox/backend/__init__.py +0 -0
- package/ftm-inbox/backend/__pycache__/main.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/adapters/__init__.py +0 -0
- package/ftm-inbox/backend/adapters/_retry.py +64 -0
- package/ftm-inbox/backend/adapters/base.py +230 -0
- package/ftm-inbox/backend/adapters/freshservice.py +104 -0
- package/ftm-inbox/backend/adapters/gmail.py +125 -0
- package/ftm-inbox/backend/adapters/jira.py +136 -0
- package/ftm-inbox/backend/adapters/registry.py +192 -0
- package/ftm-inbox/backend/adapters/slack.py +110 -0
- package/ftm-inbox/backend/db/__init__.py +0 -0
- package/ftm-inbox/backend/db/connection.py +54 -0
- package/ftm-inbox/backend/db/schema.py +78 -0
- package/ftm-inbox/backend/executor/__init__.py +7 -0
- package/ftm-inbox/backend/executor/engine.py +149 -0
- package/ftm-inbox/backend/executor/step_runner.py +98 -0
- package/ftm-inbox/backend/main.py +103 -0
- package/ftm-inbox/backend/models/__init__.py +1 -0
- package/ftm-inbox/backend/models/unified_task.py +36 -0
- package/ftm-inbox/backend/planner/__init__.py +6 -0
- package/ftm-inbox/backend/planner/__pycache__/__init__.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/generator.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/__pycache__/schema.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/planner/generator.py +127 -0
- package/ftm-inbox/backend/planner/schema.py +34 -0
- package/ftm-inbox/backend/requirements.txt +5 -0
- package/ftm-inbox/backend/routes/__init__.py +0 -0
- package/ftm-inbox/backend/routes/__pycache__/plan.cpython-314.pyc +0 -0
- package/ftm-inbox/backend/routes/execute.py +186 -0
- package/ftm-inbox/backend/routes/health.py +52 -0
- package/ftm-inbox/backend/routes/inbox.py +68 -0
- package/ftm-inbox/backend/routes/plan.py +271 -0
- package/ftm-inbox/bin/launchagent.mjs +91 -0
- package/ftm-inbox/bin/setup.mjs +188 -0
- package/ftm-inbox/bin/start.sh +10 -0
- package/ftm-inbox/bin/status.sh +17 -0
- package/ftm-inbox/bin/stop.sh +8 -0
- package/ftm-inbox/config.example.yml +55 -0
- package/ftm-inbox/package-lock.json +2898 -0
- package/ftm-inbox/package.json +26 -0
- package/ftm-inbox/postcss.config.js +6 -0
- package/ftm-inbox/src/app.css +199 -0
- package/ftm-inbox/src/app.html +18 -0
- package/ftm-inbox/src/lib/api.ts +166 -0
- package/ftm-inbox/src/lib/components/ExecutionLog.svelte +81 -0
- package/ftm-inbox/src/lib/components/InboxFeed.svelte +143 -0
- package/ftm-inbox/src/lib/components/PlanStep.svelte +271 -0
- package/ftm-inbox/src/lib/components/PlanView.svelte +206 -0
- package/ftm-inbox/src/lib/components/StreamPanel.svelte +99 -0
- package/ftm-inbox/src/lib/components/TaskCard.svelte +190 -0
- package/ftm-inbox/src/lib/components/ui/EmptyState.svelte +63 -0
- package/ftm-inbox/src/lib/components/ui/KawaiiCard.svelte +86 -0
- package/ftm-inbox/src/lib/components/ui/PillButton.svelte +106 -0
- package/ftm-inbox/src/lib/components/ui/StatusBadge.svelte +67 -0
- package/ftm-inbox/src/lib/components/ui/StreamDrawer.svelte +149 -0
- package/ftm-inbox/src/lib/components/ui/ThemeToggle.svelte +80 -0
- package/ftm-inbox/src/lib/theme.ts +47 -0
- package/ftm-inbox/src/routes/+layout.svelte +76 -0
- package/ftm-inbox/src/routes/+page.svelte +401 -0
- package/ftm-inbox/static/favicon.png +0 -0
- package/ftm-inbox/svelte.config.js +12 -0
- package/ftm-inbox/tailwind.config.ts +63 -0
- package/ftm-inbox/tsconfig.json +13 -0
- package/ftm-inbox/vite.config.ts +6 -0
- package/ftm-intent/SKILL.md +44 -0
- package/ftm-manifest.json +3794 -0
- package/ftm-map/SKILL.md +259 -0
- package/ftm-map/scripts/db.py +391 -0
- package/ftm-map/scripts/index.py +341 -0
- package/ftm-map/scripts/parser.py +455 -0
- package/ftm-map/scripts/queries/.gitkeep +0 -0
- package/ftm-map/scripts/queries/javascript-tags.scm +23 -0
- package/ftm-map/scripts/queries/python-tags.scm +17 -0
- package/ftm-map/scripts/queries/typescript-tags.scm +29 -0
- package/ftm-map/scripts/query.py +149 -0
- package/ftm-map/scripts/requirements.txt +2 -0
- package/ftm-map/scripts/setup-hooks.sh +27 -0
- package/ftm-map/scripts/setup.sh +45 -0
- package/ftm-map/scripts/test_db.py +124 -0
- package/ftm-map/scripts/test_parser.py +106 -0
- package/ftm-map/scripts/test_query.py +66 -0
- package/ftm-map/scripts/tests/fixtures/__init__.py +0 -0
- package/ftm-map/scripts/tests/fixtures/sample_project/api.ts +16 -0
- package/ftm-map/scripts/tests/fixtures/sample_project/auth.py +15 -0
- package/ftm-map/scripts/tests/fixtures/sample_project/utils.js +16 -0
- package/ftm-map/scripts/views.py +545 -0
- package/ftm-mind/SKILL.md +173 -66
- package/ftm-pause/SKILL.md +43 -0
- package/ftm-researcher/SKILL.md +275 -0
- package/ftm-researcher/evals/agent-diversity.yaml +17 -0
- package/ftm-researcher/evals/synthesis-quality.yaml +12 -0
- package/ftm-researcher/evals/trigger-accuracy.yaml +39 -0
- package/ftm-researcher/references/adaptive-search.md +116 -0
- package/ftm-researcher/references/agent-prompts.md +193 -0
- package/ftm-researcher/references/council-integration.md +193 -0
- package/ftm-researcher/references/output-format.md +203 -0
- package/ftm-researcher/references/synthesis-pipeline.md +165 -0
- package/ftm-researcher/scripts/score_credibility.py +234 -0
- package/ftm-researcher/scripts/validate_research.py +92 -0
- package/ftm-resume/SKILL.md +47 -0
- package/ftm-retro/SKILL.md +54 -0
- package/ftm-routine/SKILL.md +170 -0
- package/ftm-state/blackboard/capabilities.json +5 -0
- package/ftm-state/blackboard/capabilities.schema.json +27 -0
- package/ftm-upgrade/SKILL.md +41 -0
- package/ftm-upgrade/scripts/check-version.sh +1 -1
- package/ftm-upgrade/scripts/upgrade.sh +1 -1
- package/hooks/ftm-blackboard-enforcer.sh +94 -0
- package/hooks/ftm-discovery-reminder.sh +90 -0
- package/hooks/ftm-drafts-gate.sh +61 -0
- package/hooks/ftm-event-logger.mjs +107 -0
- package/hooks/ftm-map-autodetect.sh +79 -0
- package/hooks/ftm-pending-sync-check.sh +22 -0
- package/hooks/ftm-plan-gate.sh +96 -0
- package/hooks/ftm-post-commit-trigger.sh +57 -0
- package/hooks/settings-template.json +81 -0
- package/install.sh +140 -11
- package/package.json +12 -2
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
HOOKS_DIR="$HOME/.claude/git-hooks"
|
|
5
|
+
|
|
6
|
+
# Create hooks directory
|
|
7
|
+
mkdir -p "$HOOKS_DIR"
|
|
8
|
+
|
|
9
|
+
# Check if hooksPath is already set
|
|
10
|
+
CURRENT="$(git config --global core.hooksPath 2>/dev/null || echo "")"
|
|
11
|
+
if [ "$CURRENT" = "$HOOKS_DIR" ]; then
|
|
12
|
+
echo "core.hooksPath already configured: $HOOKS_DIR"
|
|
13
|
+
exit 0
|
|
14
|
+
fi
|
|
15
|
+
|
|
16
|
+
if [ -n "$CURRENT" ] && [ "$CURRENT" != "$HOOKS_DIR" ]; then
|
|
17
|
+
echo "WARNING: core.hooksPath is already set to: $CURRENT"
|
|
18
|
+
echo "Changing to: $HOOKS_DIR"
|
|
19
|
+
echo "Previous hooks at $CURRENT will NOT run unless manually chained."
|
|
20
|
+
fi
|
|
21
|
+
|
|
22
|
+
git config --global core.hooksPath "$HOOKS_DIR"
|
|
23
|
+
echo "Set git core.hooksPath to: $HOOKS_DIR"
|
|
24
|
+
echo ""
|
|
25
|
+
echo "NOTE: This is a GLOBAL git config change affecting all repositories."
|
|
26
|
+
echo "The post-commit hook chains to project-local hooks if they exist."
|
|
27
|
+
echo "To revert: git config --global --unset core.hooksPath"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
#!/usr/bin/env bash
|
|
2
|
+
set -euo pipefail
|
|
3
|
+
|
|
4
|
+
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)"
|
|
5
|
+
VENV_DIR="$SCRIPT_DIR/.venv"
|
|
6
|
+
|
|
7
|
+
# Check Python version (3.10+ required)
|
|
8
|
+
PYTHON_BIN=""
|
|
9
|
+
for candidate in python3.12 python3.11 python3.10 python3; do
|
|
10
|
+
if command -v "$candidate" &>/dev/null; then
|
|
11
|
+
version_ok=$("$candidate" -c "import sys; print(1 if sys.version_info >= (3, 10) else 0)" 2>/dev/null || echo 0)
|
|
12
|
+
if [ "$version_ok" = "1" ]; then
|
|
13
|
+
PYTHON_BIN="$candidate"
|
|
14
|
+
break
|
|
15
|
+
fi
|
|
16
|
+
fi
|
|
17
|
+
done
|
|
18
|
+
|
|
19
|
+
if [ -z "$PYTHON_BIN" ]; then
|
|
20
|
+
echo "ERROR: Python 3.10+ is required but not found." >&2
|
|
21
|
+
echo "Install Python 3.10 or higher and try again." >&2
|
|
22
|
+
exit 1
|
|
23
|
+
fi
|
|
24
|
+
|
|
25
|
+
PYTHON_VERSION=$("$PYTHON_BIN" -c "import sys; print(f'{sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}')")
|
|
26
|
+
echo "Using Python $PYTHON_VERSION at $("$PYTHON_BIN" -c "import sys; print(sys.executable)")"
|
|
27
|
+
|
|
28
|
+
# Create venv if it doesn't exist
|
|
29
|
+
if [ ! -d "$VENV_DIR" ]; then
|
|
30
|
+
echo "Creating virtual environment at $VENV_DIR..."
|
|
31
|
+
"$PYTHON_BIN" -m venv "$VENV_DIR"
|
|
32
|
+
echo "Virtual environment created."
|
|
33
|
+
else
|
|
34
|
+
echo "Virtual environment already exists at $VENV_DIR."
|
|
35
|
+
fi
|
|
36
|
+
|
|
37
|
+
# Install/upgrade dependencies
|
|
38
|
+
echo "Installing dependencies from requirements.txt..."
|
|
39
|
+
"$VENV_DIR/bin/pip" install -q --upgrade pip
|
|
40
|
+
"$VENV_DIR/bin/pip" install -q -r "$SCRIPT_DIR/requirements.txt"
|
|
41
|
+
echo "Dependencies installed."
|
|
42
|
+
|
|
43
|
+
# Verify the installation
|
|
44
|
+
echo "Verifying installation..."
|
|
45
|
+
"$VENV_DIR/bin/python3" -c "import sqlite_vec; from tree_sitter_language_pack import get_parser; print('ftm-map dependencies OK')"
|
|
@@ -0,0 +1,124 @@
|
|
|
1
|
+
"""Tests for ftm-map database module."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
8
|
+
from db import (
|
|
9
|
+
get_connection, add_symbol, remove_symbols_by_file, add_edge,
|
|
10
|
+
get_symbol_by_id, get_symbol_by_name, get_transitive_deps,
|
|
11
|
+
get_reverse_deps, fts_search, get_stats
|
|
12
|
+
)
|
|
13
|
+
|
|
14
|
+
class TestDatabase(unittest.TestCase):
|
|
15
|
+
def setUp(self):
|
|
16
|
+
self.tmpdir = tempfile.mkdtemp()
|
|
17
|
+
self.conn = get_connection(self.tmpdir)
|
|
18
|
+
|
|
19
|
+
def tearDown(self):
|
|
20
|
+
self.conn.close()
|
|
21
|
+
|
|
22
|
+
def test_schema_creation(self):
|
|
23
|
+
"""Tables and indexes should exist after connection."""
|
|
24
|
+
tables = [r[0] for r in self.conn.execute(
|
|
25
|
+
"SELECT name FROM sqlite_master WHERE type='table'"
|
|
26
|
+
).fetchall()]
|
|
27
|
+
self.assertIn("symbols", tables)
|
|
28
|
+
self.assertIn("edges", tables)
|
|
29
|
+
self.assertIn("symbols_fts", tables)
|
|
30
|
+
|
|
31
|
+
def test_wal_mode(self):
|
|
32
|
+
"""WAL mode should be enabled."""
|
|
33
|
+
mode = self.conn.execute("PRAGMA journal_mode").fetchone()[0]
|
|
34
|
+
self.assertEqual(mode, "wal")
|
|
35
|
+
|
|
36
|
+
def test_add_and_get_symbol(self):
|
|
37
|
+
"""Should insert and retrieve a symbol."""
|
|
38
|
+
sid = add_symbol(self.conn, "handleAuth", "function", "auth.py", 1, 5, "def handleAuth(request)", "Auth handler", "abc123")
|
|
39
|
+
self.conn.commit()
|
|
40
|
+
sym = get_symbol_by_id(self.conn, sid)
|
|
41
|
+
self.assertIsNotNone(sym)
|
|
42
|
+
self.assertEqual(sym["name"], "handleAuth")
|
|
43
|
+
self.assertEqual(sym["kind"], "function")
|
|
44
|
+
|
|
45
|
+
def test_remove_symbols_by_file(self):
|
|
46
|
+
"""Should remove all symbols for a file."""
|
|
47
|
+
add_symbol(self.conn, "foo", "function", "test.py", 1, 3)
|
|
48
|
+
add_symbol(self.conn, "bar", "function", "test.py", 5, 8)
|
|
49
|
+
add_symbol(self.conn, "baz", "function", "other.py", 1, 3)
|
|
50
|
+
self.conn.commit()
|
|
51
|
+
remove_symbols_by_file(self.conn, "test.py")
|
|
52
|
+
self.conn.commit()
|
|
53
|
+
self.assertEqual(len(get_symbol_by_name(self.conn, "foo")), 0)
|
|
54
|
+
self.assertEqual(len(get_symbol_by_name(self.conn, "baz")), 1)
|
|
55
|
+
|
|
56
|
+
def test_edges_and_cascade(self):
|
|
57
|
+
"""Edges should be deleted when source symbol is removed."""
|
|
58
|
+
s1 = add_symbol(self.conn, "caller", "function", "a.py", 1, 5)
|
|
59
|
+
s2 = add_symbol(self.conn, "callee", "function", "b.py", 1, 5)
|
|
60
|
+
add_edge(self.conn, s1, s2, "calls")
|
|
61
|
+
self.conn.commit()
|
|
62
|
+
remove_symbols_by_file(self.conn, "a.py")
|
|
63
|
+
self.conn.commit()
|
|
64
|
+
edges = self.conn.execute("SELECT * FROM edges").fetchall()
|
|
65
|
+
self.assertEqual(len(edges), 0)
|
|
66
|
+
|
|
67
|
+
def test_transitive_deps(self):
|
|
68
|
+
"""Should return transitive dependency chain."""
|
|
69
|
+
# A calls B, B calls C
|
|
70
|
+
a = add_symbol(self.conn, "A", "function", "a.py", 1, 5)
|
|
71
|
+
b = add_symbol(self.conn, "B", "function", "b.py", 1, 5)
|
|
72
|
+
c = add_symbol(self.conn, "C", "function", "c.py", 1, 5)
|
|
73
|
+
add_edge(self.conn, a, b, "calls")
|
|
74
|
+
add_edge(self.conn, b, c, "calls")
|
|
75
|
+
self.conn.commit()
|
|
76
|
+
deps = get_transitive_deps(self.conn, a)
|
|
77
|
+
dep_names = {d["name"] for d in deps}
|
|
78
|
+
self.assertIn("B", dep_names)
|
|
79
|
+
self.assertIn("C", dep_names)
|
|
80
|
+
|
|
81
|
+
def test_reverse_deps_blast_radius(self):
|
|
82
|
+
"""Blast radius of C should return B and A."""
|
|
83
|
+
a = add_symbol(self.conn, "A", "function", "a.py", 1, 5)
|
|
84
|
+
b = add_symbol(self.conn, "B", "function", "b.py", 1, 5)
|
|
85
|
+
c = add_symbol(self.conn, "C", "function", "c.py", 1, 5)
|
|
86
|
+
add_edge(self.conn, a, b, "calls")
|
|
87
|
+
add_edge(self.conn, b, c, "calls")
|
|
88
|
+
self.conn.commit()
|
|
89
|
+
blast = get_reverse_deps(self.conn, c)
|
|
90
|
+
blast_names = {d["name"] for d in blast}
|
|
91
|
+
self.assertIn("B", blast_names)
|
|
92
|
+
self.assertIn("A", blast_names)
|
|
93
|
+
|
|
94
|
+
def test_fts_search(self):
|
|
95
|
+
"""FTS5 search should rank handleAuth above getUser for 'handle' query."""
|
|
96
|
+
add_symbol(self.conn, "handleAuth", "function", "auth.py", 1, 5, "def handleAuth(request)", "Handle authentication")
|
|
97
|
+
add_symbol(self.conn, "getUser", "function", "auth.py", 7, 10, "def getUser(user_id)", "Get user by ID")
|
|
98
|
+
self.conn.commit()
|
|
99
|
+
results = fts_search(self.conn, "handle")
|
|
100
|
+
self.assertGreater(len(results), 0)
|
|
101
|
+
self.assertEqual(results[0]["name"], "handleAuth")
|
|
102
|
+
|
|
103
|
+
def test_cycle_prevention(self):
|
|
104
|
+
"""Recursive CTE should not loop on cycles."""
|
|
105
|
+
a = add_symbol(self.conn, "A", "function", "a.py", 1, 5)
|
|
106
|
+
b = add_symbol(self.conn, "B", "function", "b.py", 1, 5)
|
|
107
|
+
add_edge(self.conn, a, b, "calls")
|
|
108
|
+
add_edge(self.conn, b, a, "calls") # cycle!
|
|
109
|
+
self.conn.commit()
|
|
110
|
+
deps = get_transitive_deps(self.conn, a)
|
|
111
|
+
# Should not hang, and should contain B
|
|
112
|
+
self.assertTrue(any(d["name"] == "B" for d in deps))
|
|
113
|
+
|
|
114
|
+
def test_stats(self):
|
|
115
|
+
"""Stats should return correct counts."""
|
|
116
|
+
add_symbol(self.conn, "x", "function", "x.py", 1, 3)
|
|
117
|
+
add_symbol(self.conn, "y", "function", "y.py", 1, 3)
|
|
118
|
+
self.conn.commit()
|
|
119
|
+
stats = get_stats(self.conn)
|
|
120
|
+
self.assertEqual(stats["symbols"], 2)
|
|
121
|
+
self.assertEqual(stats["files"], 2)
|
|
122
|
+
|
|
123
|
+
if __name__ == "__main__":
|
|
124
|
+
unittest.main()
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
"""Tests for ftm-map parser module."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import unittest
|
|
5
|
+
|
|
6
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
7
|
+
|
|
8
|
+
FIXTURES_DIR = os.path.join(os.path.dirname(__file__), "tests", "fixtures", "sample_project")
|
|
9
|
+
|
|
10
|
+
class TestParser(unittest.TestCase):
|
|
11
|
+
|
|
12
|
+
@classmethod
|
|
13
|
+
def setUpClass(cls):
|
|
14
|
+
"""Check if tree-sitter-language-pack is available."""
|
|
15
|
+
try:
|
|
16
|
+
from parser import parse_file, extract_relationships, detect_language
|
|
17
|
+
cls.parse_file = staticmethod(parse_file)
|
|
18
|
+
cls.extract_relationships = staticmethod(extract_relationships)
|
|
19
|
+
cls.detect_language = staticmethod(detect_language)
|
|
20
|
+
cls.skip_reason = None
|
|
21
|
+
except ImportError as e:
|
|
22
|
+
cls.skip_reason = f"tree-sitter-language-pack not installed: {e}"
|
|
23
|
+
|
|
24
|
+
def setUp(self):
|
|
25
|
+
if self.skip_reason:
|
|
26
|
+
self.skipTest(self.skip_reason)
|
|
27
|
+
|
|
28
|
+
def test_detect_language(self):
|
|
29
|
+
self.assertEqual(self.detect_language("foo.py"), "python")
|
|
30
|
+
self.assertEqual(self.detect_language("bar.ts"), "typescript")
|
|
31
|
+
self.assertEqual(self.detect_language("baz.js"), "javascript")
|
|
32
|
+
self.assertIsNone(self.detect_language("README.md"))
|
|
33
|
+
|
|
34
|
+
def test_parse_python_file(self):
|
|
35
|
+
"""Should extract Python functions."""
|
|
36
|
+
path = os.path.join(FIXTURES_DIR, "auth.py")
|
|
37
|
+
if not os.path.exists(path):
|
|
38
|
+
self.skipTest("Fixture not found")
|
|
39
|
+
symbols = self.parse_file(path)
|
|
40
|
+
names = {s.name for s in symbols}
|
|
41
|
+
self.assertIn("handleAuth", names)
|
|
42
|
+
self.assertIn("validateToken", names)
|
|
43
|
+
self.assertIn("getUser", names)
|
|
44
|
+
|
|
45
|
+
def test_parse_typescript_file(self):
|
|
46
|
+
"""Should extract TypeScript functions and classes."""
|
|
47
|
+
path = os.path.join(FIXTURES_DIR, "api.ts")
|
|
48
|
+
if not os.path.exists(path):
|
|
49
|
+
self.skipTest("Fixture not found")
|
|
50
|
+
symbols = self.parse_file(path)
|
|
51
|
+
names = {s.name for s in symbols}
|
|
52
|
+
self.assertIn("processRequest", names)
|
|
53
|
+
self.assertIn("ApiController", names)
|
|
54
|
+
|
|
55
|
+
def test_parse_javascript_file(self):
|
|
56
|
+
"""Should extract JavaScript functions."""
|
|
57
|
+
path = os.path.join(FIXTURES_DIR, "utils.js")
|
|
58
|
+
if not os.path.exists(path):
|
|
59
|
+
self.skipTest("Fixture not found")
|
|
60
|
+
symbols = self.parse_file(path)
|
|
61
|
+
names = {s.name for s in symbols}
|
|
62
|
+
self.assertIn("formatDate", names)
|
|
63
|
+
self.assertIn("parseConfig", names)
|
|
64
|
+
|
|
65
|
+
def test_symbol_has_content_hash(self):
|
|
66
|
+
"""Every symbol should have a content hash."""
|
|
67
|
+
path = os.path.join(FIXTURES_DIR, "auth.py")
|
|
68
|
+
if not os.path.exists(path):
|
|
69
|
+
self.skipTest("Fixture not found")
|
|
70
|
+
symbols = self.parse_file(path)
|
|
71
|
+
for sym in symbols:
|
|
72
|
+
self.assertTrue(len(sym.content_hash) > 0, f"{sym.name} missing content_hash")
|
|
73
|
+
|
|
74
|
+
def test_symbol_has_line_numbers(self):
|
|
75
|
+
"""Every symbol should have start and end line."""
|
|
76
|
+
path = os.path.join(FIXTURES_DIR, "auth.py")
|
|
77
|
+
if not os.path.exists(path):
|
|
78
|
+
self.skipTest("Fixture not found")
|
|
79
|
+
symbols = self.parse_file(path)
|
|
80
|
+
for sym in symbols:
|
|
81
|
+
self.assertGreater(sym.start_line, 0)
|
|
82
|
+
self.assertGreaterEqual(sym.end_line, sym.start_line)
|
|
83
|
+
|
|
84
|
+
def test_extract_relationships(self):
|
|
85
|
+
"""Should extract call relationships."""
|
|
86
|
+
path = os.path.join(FIXTURES_DIR, "auth.py")
|
|
87
|
+
if not os.path.exists(path):
|
|
88
|
+
self.skipTest("Fixture not found")
|
|
89
|
+
rels = self.extract_relationships(path)
|
|
90
|
+
# handleAuth calls validateToken and getUser
|
|
91
|
+
call_targets = {r.target_name for r in rels if r.kind == "calls"}
|
|
92
|
+
self.assertIn("validateToken", call_targets)
|
|
93
|
+
self.assertIn("getUser", call_targets)
|
|
94
|
+
|
|
95
|
+
def test_unsupported_file_returns_empty(self):
|
|
96
|
+
"""Unsupported file types should return empty list."""
|
|
97
|
+
symbols = self.parse_file("/tmp/fake.xyz")
|
|
98
|
+
self.assertEqual(symbols, [])
|
|
99
|
+
|
|
100
|
+
def test_nonexistent_file_returns_empty(self):
|
|
101
|
+
"""Non-existent files should return empty list, not error."""
|
|
102
|
+
symbols = self.parse_file("/tmp/nonexistent_file_12345.py")
|
|
103
|
+
self.assertEqual(symbols, [])
|
|
104
|
+
|
|
105
|
+
if __name__ == "__main__":
|
|
106
|
+
unittest.main()
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"""Tests for ftm-map query module."""
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
import tempfile
|
|
5
|
+
import unittest
|
|
6
|
+
|
|
7
|
+
sys.path.insert(0, os.path.dirname(__file__))
|
|
8
|
+
from db import get_connection, add_symbol, add_edge
|
|
9
|
+
from query import blast_radius, dependency_chain, search, symbol_info
|
|
10
|
+
|
|
11
|
+
class TestQuery(unittest.TestCase):
|
|
12
|
+
def setUp(self):
|
|
13
|
+
self.tmpdir = tempfile.mkdtemp()
|
|
14
|
+
self.conn = get_connection(self.tmpdir)
|
|
15
|
+
# Build test graph: A -> B -> C, D imports A
|
|
16
|
+
self.a = add_symbol(self.conn, "handleAuth", "function", "auth.py", 1, 5, "def handleAuth(req)", "Auth handler")
|
|
17
|
+
self.b = add_symbol(self.conn, "validateToken", "function", "auth.py", 7, 10, "def validateToken(token)")
|
|
18
|
+
self.c = add_symbol(self.conn, "getUser", "function", "users.py", 1, 5, "def getUser(uid)")
|
|
19
|
+
self.d = add_symbol(self.conn, "processRequest", "function", "api.ts", 1, 8, "function processRequest(req)")
|
|
20
|
+
add_edge(self.conn, self.a, self.b, "calls")
|
|
21
|
+
add_edge(self.conn, self.a, self.c, "calls")
|
|
22
|
+
add_edge(self.conn, self.d, self.a, "calls")
|
|
23
|
+
self.conn.commit()
|
|
24
|
+
|
|
25
|
+
def tearDown(self):
|
|
26
|
+
self.conn.close()
|
|
27
|
+
|
|
28
|
+
def test_blast_radius(self):
|
|
29
|
+
"""Blast radius of getUser should include handleAuth and processRequest."""
|
|
30
|
+
result = blast_radius(self.conn, "getUser")
|
|
31
|
+
names = {r["name"] for r in result["results"]}
|
|
32
|
+
self.assertIn("handleAuth", names)
|
|
33
|
+
self.assertIn("processRequest", names)
|
|
34
|
+
self.assertEqual(result["affected_count"], 2)
|
|
35
|
+
|
|
36
|
+
def test_dependency_chain(self):
|
|
37
|
+
"""handleAuth depends on validateToken and getUser."""
|
|
38
|
+
result = dependency_chain(self.conn, "handleAuth")
|
|
39
|
+
names = {r["name"] for r in result["results"]}
|
|
40
|
+
self.assertIn("validateToken", names)
|
|
41
|
+
self.assertIn("getUser", names)
|
|
42
|
+
|
|
43
|
+
def test_search(self):
|
|
44
|
+
"""Search for 'auth' should return handleAuth."""
|
|
45
|
+
result = search(self.conn, "auth")
|
|
46
|
+
self.assertGreater(result["result_count"], 0)
|
|
47
|
+
names = {r["name"] for r in result["results"]}
|
|
48
|
+
self.assertIn("handleAuth", names)
|
|
49
|
+
|
|
50
|
+
def test_symbol_info(self):
|
|
51
|
+
"""Should return full details with callers and callees."""
|
|
52
|
+
result = symbol_info(self.conn, "handleAuth")
|
|
53
|
+
self.assertEqual(result["name"], "handleAuth")
|
|
54
|
+
callee_names = {c["name"] for c in result["callees"]}
|
|
55
|
+
self.assertIn("validateToken", callee_names)
|
|
56
|
+
self.assertIn("getUser", callee_names)
|
|
57
|
+
caller_names = {c["name"] for c in result["callers"]}
|
|
58
|
+
self.assertIn("processRequest", caller_names)
|
|
59
|
+
|
|
60
|
+
def test_missing_symbol(self):
|
|
61
|
+
"""Query for non-existent symbol should return error."""
|
|
62
|
+
result = blast_radius(self.conn, "nonexistent")
|
|
63
|
+
self.assertIn("error", result)
|
|
64
|
+
|
|
65
|
+
if __name__ == "__main__":
|
|
66
|
+
unittest.main()
|
|
File without changes
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { handleAuth } from './auth';
|
|
2
|
+
|
|
3
|
+
export function processRequest(req: Request): Response {
|
|
4
|
+
const user = handleAuth(req);
|
|
5
|
+
return formatResponse(user);
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
function formatResponse(data: any): Response {
|
|
9
|
+
return new Response(JSON.stringify(data));
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
export class ApiController {
|
|
13
|
+
handle(req: Request) {
|
|
14
|
+
return processRequest(req);
|
|
15
|
+
}
|
|
16
|
+
}
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
"""Authentication module for the sample project."""
|
|
2
|
+
|
|
3
|
+
def handleAuth(request):
|
|
4
|
+
"""Handle authentication requests."""
|
|
5
|
+
token = validateToken(request.token)
|
|
6
|
+
user = getUser(token.user_id)
|
|
7
|
+
return user
|
|
8
|
+
|
|
9
|
+
def validateToken(token):
|
|
10
|
+
"""Validate a JWT token."""
|
|
11
|
+
return {"user_id": 42, "valid": True}
|
|
12
|
+
|
|
13
|
+
def getUser(user_id):
|
|
14
|
+
"""Get user by ID from database."""
|
|
15
|
+
return {"id": user_id, "name": "Alice"}
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utility functions for the sample project.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
function formatDate(date) {
|
|
6
|
+
return date.toISOString();
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
function parseConfig(configStr) {
|
|
10
|
+
return JSON.parse(configStr);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
const processData = (data) => {
|
|
14
|
+
const config = parseConfig(data.config);
|
|
15
|
+
return { ...data, config, timestamp: formatDate(new Date()) };
|
|
16
|
+
};
|