Версия документа: 2.0.0 | Последнее обновление: 2026-07-18
Полная спецификация инструментов, которые плагин Code Shadow предоставляет AI-агенту OpenCode через экспорт
tool:в плагин-манифесте.
- Обзор
- code_shadow_analyze
- code_shadow_memory_write
- code_shadow_memory_search
- code_shadow_memory_note
- code_shadow_context_inject
- code_shadow_decide
- Рекомендации для AI-агента
- Обработка ошибок
- Полный пример регистрации плагина
Code Shadow экспортирует 7 AI-тулзов через поле tools в плагин-манифесте
OpenCode. Каждый тулз реализован через фабрику tool() из @opencode-ai/plugin-sdk
и состоит из четырёх компонентов:
| Компонент | Назначение |
|---|---|
| name | Уникальный идентификатор с префиксом code_shadow_ |
| description | Человекочитаемое описание для AI — объясняет, КОГДА и ЗАЧЕМ вызывать |
| args | Zod-схема аргументов — строгая типизация входных параметров |
| execute | Асинхронная функция (args, context) => Promise<T> — логика тулза |
Каждый тулз возвращает строго типизированный объект. Все возвращаемые структуры сериализуются в JSON и отображаются AI-агенту в читаемом виде.
Тулзы Code Shadow заменяют соответствующие инструменты Magic Context:
| Magic Context | Code Shadow | Причина замены |
|---|---|---|
ctx_memory |
code_shadow_memory_write |
Структурированное хранение в SQLite вместо текстовых файлов |
ctx_search |
code_shadow_memory_search |
Поиск по всем сущностям: память, решения, история, аналитика |
ctx_note |
code_shadow_memory_note |
Заметки с авто-триггерами на внешних условиях |
| нет аналога | code_shadow_analyze |
Полностью новый: аналитика кодовой базы |
| нет аналога | code_shadow_context_inject |
Полностью новый: инъекция контекста в сессию |
| нет аналога | code_shadow_decide |
Полностью новый: запись архитектурных решений |
import { tool } from "@opencode-ai/plugin-sdk";
tool({
description: "Анализирует здоровье кодовой базы...",
args: {
query: tool.schema.enum([
"hotspots", "predict_change", "file_history",
"team_pulse", "my_stats", "dependency_graph",
"knowledge_search", "decisions_list"
]),
target: tool.schema.string().optional(),
timeframe: tool.schema.enum(["7d", "30d", "90d", "all"]).optional(),
limit: tool.schema.number().optional(),
},
async execute(args, context) {
// 1. Валидация аргументов (дополнительная — Zod уже проверил типы)
// 2. Маршрутизация по значению args.query
// 3. SQL-запрос к data.db через Storage Engine
// 4. Формирование типизированного ответа
// 5. Возврат результата
},
});Все тулзы объединяются в единый объект экспорта:
// src/tools/index.ts
export function createAiTools(
ctx: PluginContext,
storage: StorageEngine,
analytics: AnalyticsEngine
): PluginRegistration["tools"] {
return {
code_shadow_analyze: createAnalyzeTool(ctx, storage, analytics),
code_shadow_memory_write: createMemoryWriteTool(ctx, storage),
code_shadow_memory_search: createMemorySearchTool(ctx, storage),
code_shadow_memory_note: createMemoryNoteTool(ctx, storage),
code_shadow_context_inject: createContextInjectTool(ctx, storage, analytics),
code_shadow_decide: createDecideTool(ctx, storage),
};
}Назначение: Главный аналитический тулз Code Shadow. Query-ориентированный дизайн — один тулз, 9 режимов работы. Предоставляет объективные, измеримые данные о кодовой базе, исключая догадки и субъективные мнения AI.
import { tool } from "@opencode-ai/plugin-sdk";
export function createAnalyzeTool(
ctx: PluginContext,
storage: StorageEngine,
analytics: AnalyticsEngine
) {
return tool({
description: [
"Анализирует кодовую базу. ИСПОЛЬЗУЙ АВТОМАТИЧЕСКИ перед изменениями",
"в незнакомых файлах и для понимания проекта.",
].join("\n"),
args: {
query: tool.schema.enum([
"hotspots",
"predict_change",
"file_history",
"team_pulse",
"my_stats",
"dependency_graph",
"knowledge_search",
"decisions_list",
"code_errors",
]),
target: tool.schema.string().optional(),
timeframe: tool.schema.enum(["7d", "30d", "90d", "all"]).optional(),
limit: tool.schema.number().optional(),
depth: tool.schema.number().optional(),
},
async execute(args, context) {
// Маршрутизация по типу запроса
switch (args.query) {
case "hotspots": return executeHotspots(storage, args);
case "predict_change": return executePredictChange(storage, analytics, args);
case "file_history": return executeFileHistory(storage, args);
case "team_pulse": return executeTeamPulse(storage, analytics, args);
case "my_stats": return executeMyStats(storage, args);
case "dependency_graph": return executeDependencyGraph(storage, args);
case "knowledge_search": return executeKnowledgeSearch(storage, args);
case "decisions_list": return executeDecisionsList(storage, args);
case "code_errors": return executeCodeErrors(storage, args);
default:
return { error: `Неизвестный тип запроса: ${args.query}` };
}
},
});
}Назначение: Ранжирует файлы проекта по проблемному score (0–1). Чем выше score — тем чаще файл редактируется, тем больше в нём ошибок, тем более он «фрустрирующий» для разработчика.
Параметры:
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
timeframe |
enum |
Нет | "30d" |
Окно анализа: 7, 30, 90 дней или всё |
limit |
number |
Нет | 10 |
Максимальное количество файлов в ответе |
Логика выполнения:
async function executeHotspots(
storage: StorageEngine,
args: { timeframe?: string; limit?: number }
): Promise<HotspotsResult> {
const days = parseTimeframe(args.timeframe || "30d");
const limit = args.limit || 10;
const db = storage.getDb();
const cutoff = Date.now() - days * 24 * 60 * 60 * 1000;
// 1. Собираем агрегированную статистику по каждому файлу
const rows = db.prepare(`
SELECT
fe.file_path,
COUNT(DISTINCT fe.id) AS total_edits,
COUNT(DISTINCT se.id) AS errors_caused,
COUNT(DISTINCT fe.session_id) AS sessions_touched,
COALESCE(AVG(fe.lines_added + fe.lines_removed), 0) AS avg_lines_changed,
MAX(fe.recorded_at) AS last_edit,
-- Frustration: правки одним агентом, после которых был revert
COUNT(DISTINCT de.id) AS frustration_events
FROM file_edits fe
LEFT JOIN session_errors se
ON se.context_file = fe.file_path
AND se.recorded_at >= ?
LEFT JOIN developer_events de
ON de.event_type = 'human_fix'
AND de.event_data LIKE '%' || fe.file_path || '%'
AND de.recorded_at >= ?
WHERE fe.recorded_at >= ?
GROUP BY fe.file_path
ORDER BY total_edits DESC
`).all(cutoff, cutoff, cutoff);
// 2. Нормализуем метрики и вычисляем итоговый score
const allFiles = normalizeAndScore(rows, days);
// 3. Сортируем по score и обрезаем до limit
const hotspots = allFiles
.sort((a, b) => b.score - a.score)
.slice(0, limit);
return {
hotspots,
generated_at: Date.now(),
timeframe: args.timeframe || "30d",
total_files_analyzed: allFiles.length,
};
}Формат ответа:
{
"hotspots": [
{
"file": "src/auth/session.ts",
"score": 0.87,
"breakdown": {
"edit_frequency": 0.9,
"error_rate": 0.85,
"recent_changes": 0.7,
"frustration": 0.95
},
"stats": {
"total_edits": 47,
"errors_caused": 12,
"sessions_touched": 23,
"last_edit": "2026-07-15",
"avg_lines_changed": 34
},
"explanation": "Высокая частота правок (47 за квартал) + 12 ошибок. Файл правили 23 сессии подряд — возможен конфликт архитектурных решений."
},
{
"file": "src/utils/god-object.ts",
"score": 0.74,
"breakdown": {
"edit_frequency": 0.85,
"error_rate": 0.6,
"recent_changes": 0.8,
"frustration": 0.7
},
"stats": {
"total_edits": 38,
"errors_caused": 8,
"sessions_touched": 18,
"last_edit": "2026-07-16",
"avg_lines_changed": 52
},
"explanation": "Крупные изменения (в среднем 52 строки за правку). 8 ошибок за период, высокая фрустрация — разработчик часто откатывает изменения."
}
],
"generated_at": 1752691200000,
"timeframe": "30d",
"total_files_analyzed": 87
}Интерпретация score:
| Диапазон | Уровень | Рекомендация AI |
|---|---|---|
| 0.0 – 0.3 | Низкий | Файл стабилен. Можно править без опасений. |
| 0.3 – 0.6 | Средний | Умеренный риск. Проверить тесты после изменений. |
| 0.6 – 0.8 | Высокий | Файл проблемный. Показать предупреждение пользователю. |
| 0.8 – 1.0 | Критический | Избегать изменений. Предложить рефакторинг или перепись. |
Назначение: Перед изменением файла предсказывает риск поломки билда, тестов или смежных модулей. Использует исторические данные: сколько раз изменения этого файла приводили к ошибкам, какие файлы ломались вместе с ним.
Параметры:
| Параметр | Тип | Обязательный | Описание |
|---|---|---|---|
target |
string |
ДА | Абсолютный или относительный (от workspace) путь к файлу |
Логика выполнения:
async function executePredictChange(
storage: StorageEngine,
analytics: AnalyticsEngine,
args: { target: string }
): Promise<PredictChangeResult> {
const db = storage.getDb();
// 1. Проверяем существование файла в истории
const fileStats = db.prepare(`
SELECT
COUNT(*) AS total_edits,
COUNT(DISTINCT se.id) AS total_errors,
COUNT(DISTINCT fe.session_id) AS sessions_touched
FROM file_edits fe
LEFT JOIN session_errors se
ON se.context_file = fe.file_path
WHERE fe.file_path = ?
`).get(args.target);
// 2. Вычисляем риск-метрики
const breakageRate = fileStats.total_edits > 0
? fileStats.total_errors / fileStats.total_edits
: 0;
// 3. Находим файлы, которые исторически ломались вместе с target
const coBrokenFiles = db.prepare(`
SELECT
se2.context_file AS affected_file,
COUNT(*) AS co_break_count
FROM session_errors se1
JOIN file_edits fe ON fe.file_path = ?
JOIN session_errors se2
ON se2.session_id = fe.session_id
AND se2.context_file != ?
WHERE se1.context_file = ?
GROUP BY se2.context_file
ORDER BY co_break_count DESC
LIMIT 10
`).all(args.target, args.target, args.target);
// 4. Находим зависимости через knowledge_edges
const dependencies = db.prepare(`
SELECT target_node_id, weight, relation_type
FROM knowledge_edges
WHERE source_node_id = ? AND relation_type IN ('depends_on', 'causes_bugs_in', 'coupled_with')
ORDER BY weight DESC
`).all(`file:${args.target}`);
// 5. Ищем похожие изменения в истории и их исходы
const similarChanges = db.prepare(`
SELECT
fe.file_path AS file,
date(fe.recorded_at / 1000, 'unixepoch') AS date,
CASE WHEN se.id IS NOT NULL THEN 'broke something' ELSE 'clean' END AS result,
fe.session_id
FROM file_edits fe
LEFT JOIN session_errors se
ON se.session_id = fe.session_id
AND se.context_file = fe.file_path
WHERE fe.file_path = ?
ORDER BY fe.recorded_at DESC
LIMIT 5
`).all(args.target);
// 6. Вычисляем финальный risk_score
const depsWeight = dependencies.reduce((sum, d) => sum + d.weight, 0);
const riskScore = Math.min(
(breakageRate * 0.5) +
(Math.min(depsWeight / 10, 1) * 0.3) +
(Math.min(coBrokenFiles.length / 5, 1) * 0.2),
1
);
const riskLevel = riskScore < 0.3 ? "low"
: riskScore < 0.6 ? "medium"
: riskScore < 0.8 ? "high"
: "critical";
// 7. Формируем рекомендацию
const recommendation = buildRecommendation(riskLevel, breakageRate, coBrokenFiles);
return {
target: args.target,
risk_score: Math.round(riskScore * 100) / 100,
risk_level: riskLevel,
affected_files: coBrokenFiles.map(f => f.affected_file),
dependency_count: dependencies.length,
historical_breakage_rate: Math.round(breakageRate * 100) / 100,
recent_similar_changes: similarChanges,
recommendation,
};
}Формат ответа:
{
"target": "src/db/schema.ts",
"risk_score": 0.72,
"risk_level": "high",
"affected_files": [
"src/db/migrations/005.ts",
"src/api/users.ts",
"src/api/orders.ts"
],
"dependency_count": 12,
"historical_breakage_rate": 0.6,
"recent_similar_changes": [
{
"file": "src/db/schema.ts",
"date": "2026-07-13",
"result": "broke migrations",
"session_id": "sess_abc123"
},
{
"file": "src/db/schema.ts",
"date": "2026-07-10",
"result": "broke migrations",
"session_id": "sess_def456"
},
{
"file": "src/db/schema.ts",
"date": "2026-07-01",
"result": "clean",
"session_id": "sess_ghi789"
}
],
"recommendation": "ВЫСОКИЙ РИСК. Изменения schema.ts в 60% случаев ломали миграции. Рекомендую: 1) запустить полный тест-сьют 2) проверить миграции 3) делать маленькими коммитами."
}Пороги риска и действия AI:
| Уровень | Score | Действие AI |
|---|---|---|
low |
< 0.3 | Можно править без дополнительных проверок. |
medium |
0.3–0.6 | Предупредить пользователя, предложить запустить смежные тесты. |
high |
0.6–0.8 | Показать полный список affected_files, настоять на тестах. |
critical |
> 0.8 | Заблокировать автоматическое изменение, запросить явное подтверждение. |
Назначение: Возвращает хронологию всех изменений конкретного файла: кто правил (AI build/plan или человек), когда, с каким исходом, какие ошибки возникали.
Параметры:
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
target |
string |
ДА | — | Путь к файлу |
timeframe |
enum |
Нет | "all" |
Ограничение по времени |
Формат ответа:
{
"file": "src/auth/session.ts",
"first_seen": "2025-11-03",
"total_edits": 89,
"unique_sessions": 45,
"authors": {
"ai_build": 62,
"ai_plan": 5,
"human": 22
},
"timeline": [
{
"date": "2026-07-15",
"session_id": "sess_xyz789",
"agent": "build",
"changes": "+45/-12 lines",
"caused_error": true,
"error_type": "type_error",
"error_message": "Type 'string' is not assignable to type 'number'"
},
{
"date": "2026-07-14",
"session_id": "sess_abc456",
"agent": "plan",
"changes": "+8/-2 lines",
"caused_error": false
},
{
"date": "2026-07-12",
"session_id": null,
"agent": "human",
"changes": "+23/-45 lines",
"caused_error": false
}
],
"top_collaborators": [
"src/auth/tokens.ts",
"src/middleware/auth.ts",
"src/api/login.ts"
],
"refactoring_count": 3,
"stability_score": 0.45
}Структура timeline:
| Поле | Тип | Описание |
|---|---|---|
date |
string |
Дата изменения (YYYY-MM-DD) |
session_id |
string? |
ID сессии OpenCode (null для human-изменений вне сессий) |
agent |
string |
"build" | "plan" | "human" |
changes |
string |
Форматированная строка: "+N/-M lines" |
caused_error |
boolean |
Привело ли изменение к ошибке в этой или следующей сессии |
error_type |
string? |
Тип ошибки: type_error | runtime_error | test_failure |
error_message |
string? |
Текст ошибки (если caused_error = true) |
Назначение: Даёт обзорную картину здоровья всего проекта: общий score, тренд (улучшается/ухудшается), топ проблем, динамика по месяцам.
Параметры:
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
timeframe |
enum |
Нет | "30d" |
Окно для расчёта метрик |
Формат ответа:
{
"project_health_score": 0.62,
"trend": "improving",
"total_files": 234,
"active_files": 87,
"hotspot_count": 5,
"avg_session_duration_min": 18,
"ai_reliance_ratio": 0.73,
"error_rate": 0.12,
"top_concern": "Модуль auth имеет нарастающую сложность — 3 рефакторинга за месяц не помогли",
"health_over_time": [
{ "month": "2026-05", "score": 0.58 },
{ "month": "2026-06", "score": 0.55 },
{ "month": "2026-07", "score": 0.62 }
],
"module_breakdown": [
{ "module": "src/auth", "health_score": 0.41, "files": 8, "hotspot": "session.ts" },
{ "module": "src/api", "health_score": 0.72, "files": 15, "hotspot": null },
{ "module": "src/utils", "health_score": 0.55, "files": 12, "hotspot": "god-object.ts" }
]
}Поля:
| Поле | Тип | Описание |
|---|---|---|
project_health_score |
number |
0–1, агрегированный показатель здоровья (чем выше, тем лучше) |
trend |
string |
"improving" | "stable" | "declining" |
total_files |
number |
Всего отслеживаемых файлов в проекте |
active_files |
number |
Файлы, которые редактировались за выбранный timeframe |
hotspot_count |
number |
Количество файлов со score > 0.6 |
ai_reliance_ratio |
number |
Доля изменений, внесённых AI (0 = всё руками, 1 = только AI) |
error_rate |
number |
Доля изменений, приведших к ошибке |
health_over_time |
array |
Помесячная динамика health_score — позволяет судить о тренде |
module_breakdown |
array |
Детализация по модулям первого уровня (директориям в src/) |
Назначение: Возвращает статистику текущего разработчика: количество сессий, их длительность, соотношение AI/человеческого кода, тренды, достижения.
Параметры:
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
timeframe |
enum |
Нет | "30d" |
Период анализа |
Формат ответа:
{
"sessions_total": 47,
"sessions_per_day": 2.8,
"avg_session_duration_min": 23,
"files_edited": 34,
"ai_generated_ratio": 0.68,
"human_fix_ratio": 0.22,
"top_files": [
{ "file": "src/auth/session.ts", "edits": 15, "errors": 4 },
{ "file": "src/api/payment.ts", "edits": 12, "errors": 2 },
{ "file": "src/db/schema.ts", "edits": 10, "errors": 6 }
],
"tools_used": {
"bash": 89,
"edit": 234,
"grep": 56,
"read": 312,
"write": 45,
"glob": 78
},
"errors_encountered": 8,
"trends": {
"ai_reliance": {
"previous": 0.55,
"current": 0.68,
"direction": "increasing",
"change_pct": 23.6
},
"fix_rate": {
"previous": 0.30,
"current": 0.22,
"direction": "improving",
"change_pct": -26.7
},
"session_duration": {
"previous": 18,
"current": 23,
"direction": "increasing",
"change_pct": 27.8
}
},
"achievements": [
"Fix Rate улучшился на 27% по сравнению с прошлым месяцем",
"Самый продуктивный день: 15 июля (6 сессий, 34 правки)",
"Любимый инструмент: edit (234 вызова)"
]
}Поля trends:
| Поле | Тип | Описание |
|---|---|---|
ai_reliance |
object |
Насколько выросла/упала доля AI-кода |
fix_rate |
object |
Доля правок человека поверх AI-кода (чем ниже, тем лучше AI) |
session_duration |
object |
Средняя длина сессии (мин). Растущая — возможно, задачи усложняются |
Назначение: Возвращает зависимости файла или модуля: кто его импортирует, кого импортирует он, с кем исторически менялся вместе.
Параметры:
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
target |
string |
ДА | — | Путь к файлу или модулю |
depth |
number |
Нет | 1 |
Глубина обхода графа (1–3). > 1 рекурсивно раскрывает зависимости |
Формат ответа:
{
"root": "src/auth/session.ts",
"imports": [
"src/utils/jwt.ts",
"src/db/users.ts",
"src/config/env.ts"
],
"imported_by": [
"src/middleware/auth.ts",
"src/api/login.ts",
"src/api/register.ts",
"src/api/refresh.ts"
],
"co_changed_with": [
{ "file": "src/auth/tokens.ts", "frequency": 0.85, "last_together": "2026-07-15" },
{ "file": "src/middleware/auth.ts", "frequency": 0.72, "last_together": "2026-07-14" },
{ "file": "src/db/users.ts", "frequency": 0.45, "last_together": "2026-07-10" }
],
"causes_bugs_in": [
{ "file": "src/api/login.ts", "count": 3 },
{ "file": "src/api/refresh.ts", "count": 2 }
],
"depth": 1,
"total_dependents": 4,
"total_dependencies": 3
}Поля:
| Поле | Тип | Описание |
|---|---|---|
imports |
array |
Файлы, которые импортирует target (из knowledge_edges imports) |
imported_by |
array |
Файлы, которые импортируют target (из knowledge_edges imports) |
co_changed_with |
array |
Файлы, которые исторически менялись вместе с target (из co_changes) |
causes_bugs_in |
array |
Файлы, в которых возникали ошибки после изменений target |
frequency |
number |
0–1: доля изменений target, при которых менялся и этот файл |
Назначение: Ищет по всем сущностям графа знаний Code Shadow: файлы, модули, концепции, архитектурные решения, паттерны.
Параметры:
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
target |
string |
ДА | — | Поисковый запрос (на естественном языке) |
limit |
number |
Нет | 10 |
Максимальное количество результатов |
Формат ответа:
{
"query": "authentication",
"results": [
{
"node_type": "module",
"name": "Authentication Module",
"path": "src/auth/",
"relevance": 0.95,
"description": "Handles user authentication via JWT + refresh tokens. Contains 3 files: session.ts, tokens.ts, middleware/auth.ts. Exposes 12 API endpoints."
},
{
"node_type": "decision",
"name": "ADR-003: JWT vs Session-based auth",
"relevance": 0.82,
"description": "Chose JWT for stateless scalability. Rejected session-based approach due to Redis dependency and added infrastructure complexity."
},
{
"node_type": "concept",
"name": "Token refresh flow",
"path": null,
"relevance": 0.78,
"description": "Silent refresh via short-lived access tokens (15 min) and long-lived refresh tokens (7 days). Implemented via interceptor pattern."
},
{
"node_type": "file",
"name": "src/api/login.ts",
"path": "src/api/login.ts",
"relevance": 0.71,
"description": "POST /api/auth/login endpoint. Handles credential validation, MFA challenge, and initial token pair issuance."
}
],
"total_found": 12,
"search_time_ms": 4
}Типы узлов (node_type):
| Тип | Описание | Пример |
|---|---|---|
file |
Конкретный файл в проекте | src/api/login.ts |
module |
Логический модуль (группа файлов) | src/auth/ |
concept |
Абстрактная концепция или паттерн | "Token refresh flow" |
component |
UI-компонент или сервис | "AuthService" |
api |
API-эндпоинт или интерфейс | "POST /api/auth/login" |
decision |
Архитектурное решение (ADR) | "ADR-003: JWT vs Sessions" |
Назначение: Возвращает список всех архитектурных решений (ADR),
записанных через code_shadow_decide. Опционально фильтрует по статусу.
Параметры:
| Параметр | Тип | Обязательный | По умолчанию | Описание |
|---|---|---|---|---|
target |
string |
Нет | — | Фильтр по статусу: "proposed", "accepted", "deprecated", "superseded" |
limit |
number |
Нет | 20 |
Максимальное количество записей |
Формат ответа:
{
"decisions": [
{
"id": 3,
"title": "ADR-003: JWT vs Session-based auth",
"status": "accepted",
"decided_at": "2026-01-15",
"tags": ["auth", "architecture", "security"],
"context": "Нужно было выбрать механизм аутентификации для микросервисной архитектуры",
"decision": "Использовать JWT с refresh-токенами. Access token — 15 минут, refresh — 7 дней.",
"related_files": [
"src/auth/session.ts",
"src/auth/tokens.ts",
"src/middleware/auth.ts"
]
},
{
"id": 7,
"title": "ADR-007: Переход с Zod на TypeBox",
"status": "proposed",
"decided_at": "2026-07-10",
"tags": ["validation", "performance", "types"],
"context": "Zod создаёт избыточный overhead в рантайме для высоконагруженных эндпоинтов",
"decision": "Мигрировать валидацию с Zod на TypeBox для снижения CPU-нагрузки на 30%",
"alternatives": ["Оставить Zod", "ArkType", "Valibot"],
"related_files": [
"src/validators/*.ts"
]
}
],
"total": 7
}Назначение: Записывает структурированный факт в долговременную память Code Shadow.
Полный аналог ctx_memory из Magic Context, но с хранением в SQLite (knowledge_nodes)
и авто-извлечением тегов.
import { tool } from "@opencode-ai/plugin-sdk";
export function createMemoryWriteTool(
ctx: PluginContext,
storage: StorageEngine
) {
return tool({
description: [
"Сохраняет факт о проекте. ИСПОЛЬЗУЙ КОГДА УЗНАЁШЬ важное.",
].join("\n"),
args: {
category: tool.schema.enum([
"PROJECT_RULES",
"ARCHITECTURE",
"CONSTRAINTS",
"CONFIG_VALUES",
"NAMING",
]),
content: tool.schema.string(),
},
async execute(args, context) {
const db = storage.getDb();
const now = Date.now();
// 1. Авто-извлекаем теги из содержимого
const tags = extractTags(args.content);
// 2. Генерируем уникальный ID узла
const nodeId = `memory:${hashContent(args.content)}`;
// 3. Сохраняем в knowledge_nodes
db.prepare(`
INSERT INTO knowledge_nodes (id, type, label, description, metadata, created_at, updated_at)
VALUES (?, 'concept', ?, ?, ?, ?, ?)
ON CONFLICT(id) DO UPDATE SET
description = excluded.description,
metadata = excluded.metadata,
updated_at = excluded.updated_at
`).run(
nodeId,
args.content.substring(0, 100), // label — первые 100 символов
args.content,
JSON.stringify({ category: args.category, tags, session_id: context.sessionId }),
now,
now
);
// 4. Подтверждение для AI
return {
status: "ok",
node_id: nodeId,
category: args.category,
tags,
stored_at: now,
message: `Факт сохранён в категории «${args.category}» (${args.content.length} символов).`,
};
},
});
}
/**
* Извлекает теги из текста: ищет ключевые слова, пути к файлам,
* упоминания технологий.
*/
function extractTags(content: string): string[] {
const tags: Set<string> = new Set();
// Пути к файлам: src/..., packages/..., etc.
const fileMatches = content.match(/(?:src|packages|lib|app|tests?|docs)\/[\w\/\-\.]+/gi);
fileMatches?.forEach(m => tags.add(`path:${m}`));
// Технологии: имена пакетов, фреймворков
const techPatterns = /\b(?:react|next\.?js|vue|angular|express|fastify|prisma|drizzle|zod|typebox|typescript|javascript|python|rust|golang|docker|kubernetes|postgres|mysql|redis|sqlite|graphql|rest|grpc)\b/gi;
const techMatches = content.match(techPatterns);
techMatches?.forEach(m => tags.add(`tech:${m.toLowerCase()}`));
// Версии: semver
const versionMatches = content.match(/(?:>=?|<=?|~|\^)?\d+\.\d+\.\d+/g);
versionMatches?.forEach(m => tags.add(`version:${m}`));
return [...tags].slice(0, 10); // Максимум 10 тегов
}Категории и когда их использовать:
| Категория | Назначение | Пример содержимого |
|---|---|---|
PROJECT_RULES |
Правила, которым должен следовать AI при работе с проектом | "Всегда используй import type для импорта только типов" |
ARCHITECTURE |
Факты об архитектуре (не ADR — для решений есть code_shadow_decide) | "Модуль auth — единственная точка входа для аутентификации" |
CONSTRAINTS |
Ограничения и жёсткие требования | "Node.js >= 18, нельзя использовать Node.js API старше 20" |
CONFIG_VALUES |
Конкретные значения конфигурации | "Порт по умолчанию: 3000, БД: PostgreSQL на localhost:5432" |
NAMING |
Конвенции именования | "Файлы компонентов: PascalCase.tsx, утилиты: kebab-case.ts" |
Назначение: Ищет по всей долговременной памяти Code Shadow: факты,
архитектурные решения, историю файлов, аналитические данные. Аналог ctx_search.
import { tool } from "@opencode-ai/plugin-sdk";
export function createMemorySearchTool(
ctx: PluginContext,
storage: StorageEngine
) {
return tool({
description: [
"Ищет по памяти проекта. ВЫЗЫВАЙ В НАЧАЛЕ СЕССИИ чтобы вспомнить контекст.",
].join("\n"),
args: {
query: tool.schema.string(),
sources: tool.schema.array(
tool.schema.enum(["memory", "decisions", "file_history", "analytics"])
).optional(),
limit: tool.schema.number().optional(),
},
async execute(args, context) {
const db = storage.getDb();
const query = args.query.toLowerCase();
const sources = args.sources || ["memory", "decisions", "file_history", "analytics"];
const limit = args.limit || 15;
const results: SearchResultItem[] = [];
// 1. Поиск по knowledge_nodes (память)
if (sources.includes("memory")) {
const memoryResults = db.prepare(`
SELECT id, type, label, description, metadata, updated_at
FROM knowledge_nodes
WHERE label LIKE ? OR description LIKE ?
ORDER BY updated_at DESC
LIMIT ?
`).all(`%${query}%`, `%${query}%`, limit);
for (const row of memoryResults) {
results.push({
source: "memory",
id: row.id,
type: row.type,
label: row.label,
description: row.description?.substring(0, 200),
metadata: JSON.parse(row.metadata || "{}"),
updated_at: row.updated_at,
});
}
}
// 2. Поиск по decisions (архитектурные решения)
if (sources.includes("decisions")) {
const decisionResults = db.prepare(`
SELECT id, title, context, decision, status, tags, created_at
FROM decisions
WHERE title LIKE ? OR context LIKE ? OR decision LIKE ? OR tags LIKE ?
ORDER BY created_at DESC
LIMIT ?
`).all(`%${query}%`, `%${query}%`, `%${query}%`, `%${query}%`, limit);
for (const row of decisionResults) {
results.push({
source: "decision",
id: row.id,
type: "decision",
label: row.title,
description: row.decision?.substring(0, 200),
status: row.status,
tags: JSON.parse(row.tags || "[]"),
created_at: row.created_at,
});
}
}
// 3. Поиск по file_edits (история файлов)
if (sources.includes("file_history")) {
const fileResults = db.prepare(`
SELECT DISTINCT file_path, COUNT(*) AS edit_count, MAX(recorded_at) AS last_edit
FROM file_edits
WHERE file_path LIKE ?
GROUP BY file_path
ORDER BY edit_count DESC
LIMIT ?
`).all(`%${query}%`, limit);
for (const row of fileResults) {
results.push({
source: "file_history",
type: "file",
label: row.file_path,
description: `Редактировался ${row.edit_count} раз(а). Последнее изменение: ${new Date(row.last_edit).toISOString().split("T")[0]}`,
metadata: { edit_count: row.edit_count, last_edit: row.last_edit },
});
}
}
// 4. Поиск по analytics_cache
if (sources.includes("analytics")) {
const analyticsResults = db.prepare(`
SELECT cache_key, category, numeric_value, text_value, computed_at
FROM analytics_cache
WHERE cache_key LIKE ? OR text_value LIKE ?
ORDER BY computed_at DESC
LIMIT ?
`).all(`%${query}%`, `%${query}%`, limit);
for (const row of analyticsResults) {
results.push({
source: "analytics",
id: row.cache_key,
type: row.category,
label: row.cache_key,
description: `Значение: ${row.numeric_value}${row.text_value ? ` — ${row.text_value}` : ""}`,
metadata: { computed_at: row.computed_at },
});
}
}
return {
query: args.query,
sources_queried: sources,
results: results.slice(0, limit),
total_found: results.length,
search_time_ms: 0, // будет заполнено реальным замером
};
},
});
}Формат ответа:
{
"query": "auth",
"sources_queried": ["memory", "decisions", "file_history", "analytics"],
"results": [
{
"source": "memory",
"type": "concept",
"label": "Модуль auth — единственная точка входа для аутентификации",
"description": "Модуль auth — единственная точка входа для аутентификации",
"metadata": { "category": "ARCHITECTURE", "tags": ["path:src/auth/", "tech:jwt"] },
"updated_at": 1752691200000
},
{
"source": "decision",
"type": "decision",
"id": 3,
"label": "ADR-003: JWT vs Session-based auth",
"description": "Использовать JWT с refresh-токенами. Access token — 15 минут, refresh — 7 дней.",
"status": "accepted",
"tags": ["auth", "architecture", "security"],
"created_at": 1736899200000
},
{
"source": "file_history",
"type": "file",
"label": "src/auth/session.ts",
"description": "Редактировался 89 раз(а). Последнее изменение: 2026-07-15",
"metadata": { "edit_count": 89, "last_edit": 1752691200000 }
}
],
"total_found": 12,
"search_time_ms": 4
}Назначение: Создаёт временную заметку-напоминание. Аналог ctx_note.
Поддерживает «умные заметки» — отложенное напоминание по внешнему условию
(например, «когда PR #42 будет смержен»).
import { tool } from "@opencode-ai/plugin-sdk";
export function createMemoryNoteTool(
ctx: PluginContext,
storage: StorageEngine
) {
return tool({
description: [
"Создаёт заметку. ИСПОЛЬЗУЙ В КОНЦЕ СЕССИИ для сохранения итогов.",
].join("\n"),
args: {
content: tool.schema.string(),
surface_condition: tool.schema.string().optional(),
},
async execute(args, context) {
const db = storage.getDb();
const now = Date.now();
// Сохраняем заметку в специальную таблицу
// (используем knowledge_nodes с типом 'note')
const noteId = `note:${Date.now()}_${Math.random().toString(36).slice(2, 8)}`;
db.prepare(`
INSERT INTO knowledge_nodes (id, type, label, description, metadata, created_at, updated_at)
VALUES (?, 'note', ?, ?, ?, ?, ?)
`).run(
noteId,
args.content.substring(0, 100),
args.content,
JSON.stringify({
surface_condition: args.surface_condition || null,
session_id: context.sessionId,
status: "active",
}),
now,
now
);
return {
status: "ok",
note_id: noteId,
surface_condition: args.surface_condition || null,
created_at: now,
message: args.surface_condition
? `Заметка создана. Напомню когда: ${args.surface_condition}`
: "Заметка создана.",
};
},
});
}Примеры surface_condition:
| Условие | Когда сработает |
|---|---|
"When PR #42 in xuviga/code-shadow is merged" |
После мержа PR #42 в указанном репозитории |
"When the latest release tag is >= v0.23.0" |
При достижении указанной версии |
"When packages/plugin/src/foo.ts contains a function named bar" |
При обнаружении функции bar в файле |
| Не указано | Будет показана при следующей удобной возможности |
Назначение: Внедряет релевантный контекст о проекте в текущую сессию AI-агента. Собирает информацию из всех источников Code Shadow (память, аналитика, решения) и возвращает структурированный блок для прочтения AI.
import { tool } from "@opencode-ai/plugin-sdk";
export function createContextInjectTool(
ctx: PluginContext,
storage: StorageEngine,
analytics: AnalyticsEngine
) {
return tool({
description: [
"Внедряет контекст проекта в сессию. ВЫЗЫВАЙ В НАЧАЛЕ СЕССИИ.",
].join("\n"),
args: {
focus: tool.schema.string().optional(),
include: tool.schema.array(
tool.schema.enum([
"architecture",
"conventions",
"hotspots",
"recent_changes",
"decisions",
])
).optional(),
},
async execute(args, context) {
const db = storage.getDb();
const include = args.include || [
"architecture", "conventions", "hotspots", "recent_changes", "decisions"
];
const contextBlocks: string[] = [];
// 1. Архитектурные знания
if (include.includes("architecture")) {
const archNodes = db.prepare(`
SELECT label, description FROM knowledge_nodes
WHERE type = 'module' OR metadata LIKE '%ARCHITECTURE%'
ORDER BY updated_at DESC LIMIT 10
`).all();
if (archNodes.length > 0) {
contextBlocks.push("## Архитектура проекта\n");
for (const node of archNodes) {
contextBlocks.push(`- **${node.label}**: ${node.description}`);
}
}
}
// 2. Конвенции (правила, именования, конфиги)
if (include.includes("conventions")) {
const conventionNodes = db.prepare(`
SELECT label, description, metadata FROM knowledge_nodes
WHERE metadata LIKE '%PROJECT_RULES%'
OR metadata LIKE '%NAMING%'
OR metadata LIKE '%CONFIG_VALUES%'
OR metadata LIKE '%CONSTRAINTS%'
ORDER BY updated_at DESC LIMIT 15
`).all();
if (conventionNodes.length > 0) {
contextBlocks.push("\n## Конвенции и ограничения\n");
for (const node of conventionNodes) {
contextBlocks.push(`- ${node.description}`);
}
}
}
// 3. Горячие точки
if (include.includes("hotspots")) {
const hotspots = db.prepare(`
SELECT cache_key, numeric_value FROM analytics_cache
WHERE category = 'hotspot' AND numeric_value > 0.5
ORDER BY numeric_value DESC LIMIT 5
`).all();
if (hotspots.length > 0) {
contextBlocks.push("\n## Проблемные файлы (hotspots)\n");
for (const h of hotspots) {
const fileName = h.cache_key.replace("hotspot:", "");
contextBlocks.push(`- \`${fileName}\` — score: ${h.numeric_value.toFixed(2)}`);
}
}
}
// 4. Недавние изменения
if (include.includes("recent_changes")) {
const recentChanges = db.prepare(`
SELECT DISTINCT file_path, MAX(recorded_at) as last_edit, COUNT(*) as cnt
FROM file_edits
WHERE recorded_at >= ?
GROUP BY file_path
ORDER BY cnt DESC LIMIT 10
`).all(Date.now() - 7 * 24 * 60 * 60 * 1000);
if (recentChanges.length > 0) {
contextBlocks.push("\n## Недавние изменения (7 дней)\n");
for (const rc of recentChanges) {
contextBlocks.push(`- \`${rc.file_path}\` — ${rc.cnt} правок`);
}
}
}
// 5. Архитектурные решения
if (include.includes("decisions")) {
const decisions = db.prepare(`
SELECT title, decision, status FROM decisions
WHERE status = 'accepted'
ORDER BY created_at DESC LIMIT 5
`).all();
if (decisions.length > 0) {
contextBlocks.push("\n## Ключевые архитектурные решения\n");
for (const d of decisions) {
contextBlocks.push(`- **${d.title}** [${d.status}]: ${d.decision}`);
}
}
}
// 6. Если указан focus — добавляем информацию о конкретном модуле/файле
if (args.focus) {
const focusInfo = await gatherFocusContext(db, analytics, args.focus);
if (focusInfo) {
contextBlocks.push(`\n## Контекст для: ${args.focus}\n`);
contextBlocks.push(focusInfo);
}
}
return {
injected_at: Date.now(),
sections_count: contextBlocks.filter(b => b.startsWith("##")).length,
context: contextBlocks.join("\n"),
focus: args.focus || null,
message: `Контекст проекта внедрён. ${contextBlocks.filter(b => b.startsWith("##")).length} разделов.`,
};
},
});
}Формат ответа:
{
"injected_at": 1752691200000,
"sections_count": 5,
"context": "## Архитектура проекта\n- **Authentication Module**: Handles...\n\n## Конвенции и ограничения\n- Всегда используй `import type`...\n\n## Проблемные файлы (hotspots)\n- `src/auth/session.ts` — score: 0.87\n\n## Недавние изменения (7 дней)\n- `src/auth/session.ts` — 8 правок\n\n## Ключевые архитектурные решения\n- **ADR-003: JWT vs Session-based auth** [accepted]: ...",
"focus": null,
"message": "Контекст проекта внедрён. 5 разделов."
}Назначение: Записывает архитектурное решение (ADR — Architecture Decision Record) в базу Code Shadow. Сохраняет полный контекст: что привело к решению, какие были альтернативы, какие файлы затронуты.
import { tool } from "@opencode-ai/plugin-sdk";
export function createDecideTool(
ctx: PluginContext,
storage: StorageEngine
) {
return tool({
description: [
"Записывает архитектурное решение. ИСПОЛЬЗУЙ для важных технических решений.",
].join("\n"),
args: {
title: tool.schema.string(),
description: tool.schema.string(),
context: tool.schema.string().optional(),
alternatives: tool.schema.array(tool.schema.string()).optional(),
status: tool.schema.enum(["proposed", "accepted", "deprecated", "superseded"]).optional(),
related_files: tool.schema.array(tool.schema.string()).optional(),
tags: tool.schema.array(tool.schema.string()).optional(),
},
async execute(args, context) {
const db = storage.getDb();
const now = Date.now();
// 1. Сохраняем решение в таблицу decisions
const result = db.prepare(`
INSERT INTO decisions (title, context, decision, consequences, status, tags, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
args.title,
args.context || null,
args.description,
null, // consequences — опционально, можно добавить позже
args.status || "proposed",
JSON.stringify(args.tags || []),
now,
now
);
const decisionId = result.lastInsertRowid;
// 2. Сохраняем альтернативы как отдельную мета-запись
if (args.alternatives && args.alternatives.length > 0) {
db.prepare(`
INSERT INTO knowledge_nodes (id, type, label, description, metadata, created_at, updated_at)
VALUES (?, 'concept', ?, ?, ?, ?, ?)
`).run(
`decision_alt:${decisionId}`,
`Альтернативы для: ${args.title}`,
args.alternatives.join("; "),
JSON.stringify({ decision_id: decisionId, type: "alternatives" }),
now,
now
);
}
// 3. Связываем решение с файлами через knowledge_edges
if (args.related_files && args.related_files.length > 0) {
const insertEdge = db.prepare(`
INSERT OR IGNORE INTO knowledge_edges (source_node_id, target_node_id, relation_type, weight, evidence, created_at)
VALUES (?, ?, 'references', 0.8, 'manual', ?)
`);
const decisionNodeId = `decision:${decisionId}`;
// Регистрируем решение как узел графа
db.prepare(`
INSERT OR IGNORE INTO knowledge_nodes (id, type, label, description, metadata, created_at, updated_at)
VALUES (?, 'decision', ?, ?, ?, ?, ?)
`).run(
decisionNodeId,
args.title,
args.description,
JSON.stringify({
status: args.status || "proposed",
tags: args.tags || [],
alternatives: args.alternatives || [],
}),
now,
now
);
for (const file of args.related_files) {
insertEdge.run(decisionNodeId, `file:${file}`, now);
}
}
// 4. Авто-извлекаем теги если не указаны явно
const finalTags = args.tags && args.tags.length > 0
? args.tags
: extractTags(`${args.title} ${args.description}`);
return {
status: "ok",
decision_id: decisionId,
title: args.title,
status: args.status || "proposed",
tags: finalTags,
related_files_count: args.related_files?.length || 0,
created_at: now,
message: `Архитектурное решение «${args.title}» записано (ID: ${decisionId}).`,
};
},
});
}Формат ответа:
{
"status": "ok",
"decision_id": 12,
"title": "ADR-012: Переход с REST на GraphQL для BFF-слоя",
"status": "proposed",
"tags": ["api", "graphql", "architecture", "bff"],
"related_files_count": 5,
"created_at": 1752691200000,
"message": "Архитектурное решение «ADR-012: Переход с REST на GraphQL для BFF-слоя» записано (ID: 12)."
}Статусы решений:
| Статус | Описание |
|---|---|
proposed |
Решение предложено, но ещё не принято окончательно |
accepted |
Решение принято и реализуется |
deprecated |
Решение более не актуально (но проект всё ещё его использует) |
superseded |
Решение заменено другим (указать каким в consequences) |
┌─────────────────────────────────────────────────────────────────────┐
│ ДЕРЕВО ПРИНЯТИЯ РЕШЕНИЙ ДЛЯ AI │
├─────────────────────────────────────────────────────────────────────┤
│ │
│ Ты начинаешь новую сессию на знакомом проекте? │
│ ├── ДА → code_shadow_context_inject │
│ └── НЕТ → продолжаем │
│ │
│ Тебе нужно понять, насколько рискованно менять файл? │
│ ├── ДА → code_shadow_analyze (query: "predict_change") │
│ └── НЕТ → продолжаем │
│ │
│ Тебе нужно узнать «горячие точки» в проекте? │
│ ├── ДА → code_shadow_analyze (query: "hotspots") │
│ └── НЕТ → продолжаем │
│ │
│ Ты узнал важный факт о проекте, который нужно запомнить НАВСЕГДА? │
│ ├── ДА → code_shadow_memory_write │
│ └── НЕТ → продолжаем │
│ │
│ Ты принял важное архитектурное решение? │
│ ├── ДА → code_shadow_decide │
│ └── НЕТ → продолжаем │
│ │
│ Ты хочешь найти информацию по ключевому слову во всей памяти? │
│ ├── ДА → code_shadow_memory_search │
│ └── НЕТ → продолжаем │
│ │
│ Тебе нужно создать отложенное напоминание? │
│ ├── ДА → code_shadow_memory_note │
│ └── НЕТ → продолжаем │
│ │
│ Тебе нужна детальная информация о конкретном файле/модуле? │
│ ├── ДА → code_shadow_analyze (query: "file_history" / │
│ │ "dependency_graph" / "knowledge_search" / "decisions_list")│
│ └── НЕТ → используй обычные инструменты (read, grep, glob) │
│ │
│ Тебе нужна общая картина проекта или личная статистика? │
│ ├── ДА → code_shadow_analyze (query: "team_pulse" / "my_stats") │
│ └── НЕТ → используй обычные инструменты │
│ │
└─────────────────────────────────────────────────────────────────────┘
-
Перед редактированием незнакомого файла:
code_shadow_analyze(query: "predict_change", target: "путь/к/файлу.ts")Это САМЫЙ важный вызов. Он спасёт от поломки билда.
-
При входе в новый проект или после долгого перерыва:
code_shadow_context_inject(include: ["architecture", "conventions", "decisions"])Это ВТОРОЙ по важности. Экономит 5-10 минут на исследование проекта.
-
Когда узнаёшь конвенцию или правило:
code_shadow_memory_write(category: "NAMING", content: "...")Записывай СРАЗУ, не откладывай. Иначе забудешь.
-
Когда принимаешь решение о выборе технологии:
code_shadow_decide(title: "...", description: "...", alternatives: [...])Особенно если обсуждал несколько вариантов — сохрани альтернативы.
| ❌ Плохо | ✅ Хорошо |
|---|---|
Вызывать analyze для поиска строки в файле |
Использовать grep для поиска строки |
Вызывать memory_write для временной заметки |
Использовать memory_note для временных заметок |
Вызывать decide для решения «назвать переменную x» |
Использовать decide только для нетривиальных архитектурных решений |
Вызывать context_inject КАЖДЫЙ раз в одной сессии |
Вызвать ОДИН раз в начале сессии |
Игнорировать predict_change перед правкой schema.ts |
ВСЕГДА проверять риск перед изменением файлов со score > 0.5 |
Вызывать analyze (query: "hotspots") и не показывать результат пользователю |
Показывать краткую сводку: «Вот 3 самых проблемных файла в проекте» |
Все тулзы Code Shadow возвращают ошибки в едином формате:
{
"error": true,
"code": "SHADOW_ERR_DATABASE_LOCKED",
"message": "База данных временно заблокирована. Повторите запрос через 1-2 секунды.",
"details": {
"tool": "code_shadow_analyze",
"query_type": "hotspots",
"retryable": true,
"retry_after_ms": 1000
}
}| Код | HTTP-аналог | Описание | Повтор? |
|---|---|---|---|
SHADOW_ERR_DATABASE_LOCKED |
503 | SQLite заблокирован другим писателем (WAL-конфликт) | Да, через 1-2 сек |
SHADOW_ERR_DATABASE_CORRUPT |
500 | Файл data.db повреждён | Нет (требуется ручное восстановление) |
SHADOW_ERR_NOT_INITIALIZED |
503 | Плагин ещё не завершил инициализацию БД | Да, через 5 сек |
SHADOW_ERR_INSUFFICIENT_DATA |
404 | Недостаточно данных для анализа (слишком мало событий) | Да, после накопления данных |
SHADOW_ERR_TIMEOUT |
504 | Запрос превысил допустимое время выполнения (5 сек) | Да, с меньшим timeframe |
| Код | Описание |
|---|---|
SHADOW_ERR_MISSING_TARGET |
Для запроса типа predict_change, file_history, dependency_graph не указан target |
SHADOW_ERR_INVALID_QUERY |
Передан неизвестный тип query (не из enum) |
SHADOW_ERR_INVALID_TIMEFRAME |
Передан неизвестный timeframe (не 7d, 30d, 90d, all) |
SHADOW_ERR_INVALID_LIMIT |
limit меньше 1 или больше 100 |
SHADOW_ERR_UNKNOWN_CATEGORY |
category не из enum для memory_write |
SHADOW_ERR_INVALID_SOURCES |
Один из sources не из допустимого enum |
| Код | Описание |
|---|---|
SHADOW_ERR_FILE_NOT_FOUND |
target-файл не найден в истории правок |
SHADOW_ERR_NO_HOTSPOTS |
Ни один файл не набрал score > 0.1 (слишком стабильный проект) |
SHADOW_ERR_NO_PREDICTION_DATA |
Для predict_change нет исторических данных по файлу |
SHADOW_ERR_NO_DEPENDENCIES |
Для dependency_graph не найдено зависимостей (файл изолирован) |
SHADOW_ERR_EMPTY_KNOWLEDGE_GRAPH |
Граф знаний пуст (плагин только что установлен) |
SHADOW_ERR_NO_DECISIONS |
Нет записанных архитектурных решений |
SHADOW_ERR_SEARCH_TOO_BROAD |
Поисковый запрос слишком короткий (< 2 символов) или слишком общий |
| Код | Описание |
|---|---|
SHADOW_ERR_CONTENT_TOO_SHORT |
content короче 10 символов |
SHADOW_ERR_CONTENT_TOO_LONG |
content длиннее 10000 символов |
SHADOW_ERR_DUPLICATE_MEMORY |
Точно такой же факт уже существует (конфликт по хешу) |
| Код | Описание |
|---|---|
SHADOW_ERR_SEARCH_QUERY_EMPTY |
query пустой или содержит только пробелы |
SHADOW_ERR_NO_SOURCES |
sources — пустой массив (не указано, где искать) |
| Код | Описание |
|---|---|
SHADOW_ERR_TITLE_TOO_SHORT |
title короче 5 символов |
SHADOW_ERR_DESCRIPTION_TOO_SHORT |
description короче 20 символов |
SHADOW_ERR_DECISION_EXISTS |
Решение с таким title уже существует |
| Код | Описание |
|---|---|
SHADOW_ERR_NO_CONTEXT_AVAILABLE |
Нет доступного контекста (плагин только установлен, данных нет) |
SHADOW_ERR_INVALID_FOCUS |
Указанный focus не соответствует ни одному файлу или модулю |
// src/tools/error-handling.ts
export class ShadowToolError extends Error {
constructor(
public code: string,
message: string,
public details: Record<string, unknown> = {},
public retryable: boolean = false
) {
super(message);
this.name = "ShadowToolError";
}
toJSON() {
return {
error: true,
code: this.code,
message: this.message,
details: {
...this.details,
retryable: this.retryable,
timestamp: Date.now(),
},
};
}
}
export function wrapToolExecute<T>(
toolName: string,
fn: () => Promise<T>
): Promise<T | ShadowToolError> {
return Promise.resolve()
.then(() => fn())
.catch((err) => {
// Известные ошибки SQLite
if (err.message?.includes("SQLITE_BUSY")) {
return new ShadowToolError(
"SHADOW_ERR_DATABASE_LOCKED",
"База данных временно заблокирована. Повторите запрос через 1-2 секунды.",
{ tool: toolName },
true
);
}
if (err.message?.includes("SQLITE_CORRUPT")) {
return new ShadowToolError(
"SHADOW_ERR_DATABASE_CORRUPT",
"Файл базы данных повреждён. Запустите `opencode shadow repair`.",
{ tool: toolName },
false
);
}
// Таймаут
if (err.name === "TimeoutError") {
return new ShadowToolError(
"SHADOW_ERR_TIMEOUT",
`Тулз ${toolName} превысил лимит времени выполнения. Попробуйте уменьшить timeframe или limit.`,
{ tool: toolName },
true
);
}
// Неизвестная ошибка: логируем и возвращаем safe-ответ
console.error(`[CodeShadow] Unhandled error in ${toolName}:`, err);
return new ShadowToolError(
"SHADOW_ERR_INTERNAL",
`Внутренняя ошибка при выполнении ${toolName}.`,
{ tool: toolName, original_error: err.message },
false
);
});
}Пример использования wrapToolExecute:
async execute(args, context) {
return wrapToolExecute("code_shadow_analyze", async () => {
// Основная логика тулза
switch (args.query) {
case "hotspots": return await executeHotspots(storage, args);
// ...
}
});
}Ниже представлен полный файл src/tools/index.ts, собирающий все 7 тулзов
в единый экспорт. Именно этот объект попадает в tools: плагин-манифеста.
// src/tools/index.ts
// ============================================================
// AI Tools Interface — регистрация всех тулзов Code Shadow
// ============================================================
import type { PluginContext, PluginRegistration } from "@opencode-ai/plugin-sdk";
import { tool } from "@opencode-ai/plugin-sdk";
import type { StorageEngine } from "../storage";
import type { AnalyticsEngine } from "../analytics";
import { wrapToolExecute } from "./error-handling";
import {
executeHotspots,
executePredictChange,
executeFileHistory,
executeTeamPulse,
executeMyStats,
executeDependencyGraph,
executeKnowledgeSearch,
executeDecisionsList,
} from "./analyze-handlers";
import { extractTags, hashContent } from "./utils";
// ============================================================
// 1. code_shadow_analyze — главный аналитический тулз
// ============================================================
function createAnalyzeTool(
ctx: PluginContext,
storage: StorageEngine,
analytics: AnalyticsEngine
) {
return tool({
description: [
"Анализирует здоровье кодовой базы: hotspots (проблемные файлы),",
"предикшны риска изменений, историю файлов, статистику разработчика,",
"граф зависимостей, семантический поиск по знаниям, список архитектурных",
"решений и общую картину здоровья проекта.",
"",
"ИСПОЛЬЗУЙ для получения ОБЪЕКТИВНЫХ ДАННЫХ о проекте, а не догадок.",
"НЕ используй когда достаточно простого grep/read — этот тулз для",
"аналитики, а не для поиска по коду.",
].join("\n"),
args: {
query: tool.schema.enum([
"hotspots",
"predict_change",
"file_history",
"team_pulse",
"my_stats",
"dependency_graph",
"knowledge_search",
"decisions_list",
]),
target: tool.schema.string().optional(),
timeframe: tool.schema.enum(["7d", "30d", "90d", "all"]).optional(),
limit: tool.schema.number().optional(),
},
async execute(args, context) {
return wrapToolExecute("code_shadow_analyze", async () => {
switch (args.query) {
case "hotspots":
return executeHotspots(storage, {
timeframe: args.timeframe,
limit: args.limit,
});
case "predict_change":
if (!args.target) {
throw new ShadowToolError(
"SHADOW_ERR_MISSING_TARGET",
'Для query="predict_change" необходимо указать target (путь к файлу).',
{ query: args.query }
);
}
return executePredictChange(storage, analytics, {
target: args.target,
});
case "file_history":
if (!args.target) {
throw new ShadowToolError(
"SHADOW_ERR_MISSING_TARGET",
'Для query="file_history" необходимо указать target (путь к файлу).',
{ query: args.query }
);
}
return executeFileHistory(storage, {
target: args.target,
timeframe: args.timeframe,
});
case "team_pulse":
return executeTeamPulse(storage, analytics, {
timeframe: args.timeframe,
});
case "my_stats":
return executeMyStats(storage, {
timeframe: args.timeframe,
});
case "dependency_graph":
if (!args.target) {
throw new ShadowToolError(
"SHADOW_ERR_MISSING_TARGET",
'Для query="dependency_graph" необходимо указать target (путь к файлу или модулю).',
{ query: args.query }
);
}
return executeDependencyGraph(storage, {
target: args.target,
depth: args.limit || 1,
});
case "knowledge_search":
if (!args.target) {
throw new ShadowToolError(
"SHADOW_ERR_MISSING_TARGET",
'Для query="knowledge_search" необходимо указать target (поисковый запрос).',
{ query: args.query }
);
}
return executeKnowledgeSearch(storage, {
query: args.target,
limit: args.limit,
});
case "decisions_list":
return executeDecisionsList(storage, {
status: args.target as string | undefined,
limit: args.limit,
});
default:
throw new ShadowToolError(
"SHADOW_ERR_INVALID_QUERY",
`Неизвестный тип запроса: ${args.query}`,
{ query: args.query }
);
}
});
},
});
}
// ============================================================
// 2. code_shadow_memory_write — запись факта в долговременную память
// ============================================================
function createMemoryWriteTool(ctx: PluginContext, storage: StorageEngine) {
return tool({
description: [
"Сохраняет факт о проекте в долговременную память Code Shadow.",
"Заменяет ctx_memory из Magic Context.",
"",
"ИСПОЛЬЗУЙ когда узнаёшь что-то важное о проекте:",
"- Пути к ключевым файлам",
"- Конфигурационные значения",
"- Архитектурные конвенции",
"- Именования (camelCase vs snake_case, префиксы и суффиксы)",
"- Ограничения проекта (Node >= 18, нельзя использовать X)",
"",
"НЕ ИСПОЛЬЗУЙ для временных заметок — для них есть code_shadow_memory_note.",
"НЕ ИСПОЛЬЗУЙ для архитектурных решений — для них есть code_shadow_decide.",
].join("\n"),
args: {
category: tool.schema.enum([
"PROJECT_RULES",
"ARCHITECTURE",
"CONSTRAINTS",
"CONFIG_VALUES",
"NAMING",
]),
content: tool.schema.string(),
},
async execute(args, context) {
return wrapToolExecute("code_shadow_memory_write", async () => {
// Валидация
if (args.content.length < 10) {
throw new ShadowToolError(
"SHADOW_ERR_CONTENT_TOO_SHORT",
"Содержимое должно быть не короче 10 символов.",
{ length: args.content.length }
);
}
if (args.content.length > 10000) {
throw new ShadowToolError(
"SHADOW_ERR_CONTENT_TOO_LONG",
"Содержимое не должно превышать 10000 символов.",
{ length: args.content.length }
);
}
const db = storage.getDb();
const now = Date.now();
const tags = extractTags(args.content);
const nodeId = `memory:${hashContent(args.content)}`;
// Проверка на дубликат
const existing = db.prepare(
"SELECT id FROM knowledge_nodes WHERE id = ?"
).get(nodeId);
if (existing) {
// Обновляем существующую запись
db.prepare(`
UPDATE knowledge_nodes
SET description = ?, metadata = ?, updated_at = ?
WHERE id = ?
`).run(
args.content,
JSON.stringify({
category: args.category,
tags,
session_id: context.sessionId,
}),
now,
nodeId
);
return {
status: "ok",
node_id: nodeId,
category: args.category,
tags,
updated_at: now,
message: `Факт обновлён в категории «${args.category}» (${args.content.length} символов).`,
};
}
// Новая запись
db.prepare(`
INSERT INTO knowledge_nodes (id, type, label, description, metadata, created_at, updated_at)
VALUES (?, 'concept', ?, ?, ?, ?, ?)
`).run(
nodeId,
args.content.substring(0, 100),
args.content,
JSON.stringify({
category: args.category,
tags,
session_id: context.sessionId,
}),
now,
now
);
return {
status: "ok",
node_id: nodeId,
category: args.category,
tags,
stored_at: now,
message: `Факт сохранён в категории «${args.category}» (${args.content.length} символов).`,
};
});
},
});
}
// ============================================================
// 3. code_shadow_memory_search — поиск по всей памяти
// ============================================================
function createMemorySearchTool(ctx: PluginContext, storage: StorageEngine) {
return tool({
description: [
"Ищет по всей долговременной памяти Code Shadow: факты, решения,",
"историю файлов, паттерны. Заменяет ctx_search из Magic Context.",
"",
"ИСПОЛЬЗУЙ когда нужно найти информацию о модуле, файле, архитектурном",
"решении или паттерне в проекте.",
].join("\n"),
args: {
query: tool.schema.string(),
sources: tool.schema.array(
tool.schema.enum(["memory", "decisions", "file_history", "analytics"])
).optional(),
limit: tool.schema.number().optional(),
},
async execute(args, context) {
return wrapToolExecute("code_shadow_memory_search", async () => {
if (!args.query || args.query.trim().length === 0) {
throw new ShadowToolError(
"SHADOW_ERR_SEARCH_QUERY_EMPTY",
"Поисковый запрос не может быть пустым."
);
}
const db = storage.getDb();
const query = args.query.toLowerCase();
const sources = args.sources || ["memory", "decisions", "file_history", "analytics"];
const limit = args.limit || 15;
const results: Record<string, unknown>[] = [];
if (sources.includes("memory")) {
const rows = db.prepare(`
SELECT id, type, label, description, metadata, updated_at
FROM knowledge_nodes
WHERE (label LIKE ? OR description LIKE ?) AND type != 'note'
ORDER BY updated_at DESC LIMIT ?
`).all(`%${query}%`, `%${query}%`, limit);
for (const row of rows) {
results.push({ source: "memory", ...row });
}
}
if (sources.includes("decisions")) {
const rows = db.prepare(`
SELECT id, title AS label, decision AS description, status, tags, created_at
FROM decisions
WHERE title LIKE ? OR decision LIKE ? OR tags LIKE ?
ORDER BY created_at DESC LIMIT ?
`).all(`%${query}%`, `%${query}%`, `%${query}%`, limit);
for (const row of rows) {
results.push({ source: "decision", type: "decision", ...row });
}
}
if (sources.includes("file_history")) {
const rows = db.prepare(`
SELECT DISTINCT file_path AS label, COUNT(*) AS edit_count,
MAX(recorded_at) AS last_edit
FROM file_edits WHERE file_path LIKE ?
GROUP BY file_path ORDER BY edit_count DESC LIMIT ?
`).all(`%${query}%`, limit);
for (const row of rows) {
results.push({
source: "file_history",
type: "file",
label: row.label,
description: `${row.edit_count} правок, последняя: ${new Date(row.last_edit).toISOString().slice(0, 10)}`,
metadata: { edit_count: row.edit_count, last_edit: row.last_edit },
});
}
}
if (sources.includes("analytics")) {
const rows = db.prepare(`
SELECT cache_key AS id, category AS type, cache_key AS label,
numeric_value, text_value, computed_at
FROM analytics_cache
WHERE cache_key LIKE ? OR text_value LIKE ?
ORDER BY computed_at DESC LIMIT ?
`).all(`%${query}%`, `%${query}%`, limit);
for (const row of rows) {
results.push({ source: "analytics", ...row });
}
}
return {
query: args.query,
sources_queried: sources,
results: results.slice(0, limit),
total_found: results.length,
search_time_ms: 0,
};
});
},
});
}
// ============================================================
// 4. code_shadow_memory_note — создание заметки-напоминания
// ============================================================
function createMemoryNoteTool(ctx: PluginContext, storage: StorageEngine) {
return tool({
description: [
"Создаёт заметку-напоминание в Code Shadow.",
"Заменяет ctx_note из Magic Context.",
"",
"ИСПОЛЬЗУЙ для отложенных задач и напоминаний.",
"НЕ ИСПОЛЬЗУЙ для постоянных знаний — для них есть code_shadow_memory_write.",
].join("\n"),
args: {
content: tool.schema.string(),
surface_condition: tool.schema.string().optional(),
},
async execute(args, context) {
return wrapToolExecute("code_shadow_memory_note", async () => {
const db = storage.getDb();
const now = Date.now();
const noteId = `note:${now}_${Math.random().toString(36).slice(2, 8)}`;
db.prepare(`
INSERT INTO knowledge_nodes (id, type, label, description, metadata, created_at, updated_at)
VALUES (?, 'note', ?, ?, ?, ?, ?)
`).run(
noteId,
args.content.substring(0, 100),
args.content,
JSON.stringify({
surface_condition: args.surface_condition || null,
session_id: context.sessionId,
status: "active",
}),
now,
now
);
return {
status: "ok",
note_id: noteId,
surface_condition: args.surface_condition || null,
created_at: now,
message: args.surface_condition
? `Заметка создана. Напомню когда: ${args.surface_condition}`
: "Заметка создана.",
};
});
},
});
}
// ============================================================
// 5. code_shadow_context_inject — инъекция контекста проекта
// ============================================================
function createContextInjectTool(
ctx: PluginContext,
storage: StorageEngine,
analytics: AnalyticsEngine
) {
return tool({
description: [
"Внедряет релевантный контекст о проекте в текущую сессию из всех",
"источников Code Shadow: память, аналитика, решения, история.",
"",
"ИСПОЛЬЗУЙ в начале новой сессии на знакомом проекте.",
"ИСПОЛЬЗУЙ с focus для контекста по конкретному модулю.",
].join("\n"),
args: {
focus: tool.schema.string().optional(),
include: tool.schema.array(
tool.schema.enum([
"architecture",
"conventions",
"hotspots",
"recent_changes",
"decisions",
])
).optional(),
},
async execute(args, context) {
return wrapToolExecute("code_shadow_context_inject", async () => {
const db = storage.getDb();
const include = args.include || [
"architecture", "conventions", "hotspots", "recent_changes", "decisions"
];
const blocks: string[] = [];
if (include.includes("architecture")) {
const nodes = db.prepare(`
SELECT label, description FROM knowledge_nodes
WHERE type = 'module' OR metadata LIKE '%ARCHITECTURE%'
ORDER BY updated_at DESC LIMIT 10
`).all();
if (nodes.length > 0) {
blocks.push("## Архитектура\n" + nodes.map(n => `- **${n.label}**: ${n.description}`).join("\n"));
}
}
if (include.includes("conventions")) {
const nodes = db.prepare(`
SELECT description FROM knowledge_nodes
WHERE metadata LIKE '%PROJECT_RULES%'
OR metadata LIKE '%NAMING%'
OR metadata LIKE '%CONFIG_VALUES%'
OR metadata LIKE '%CONSTRAINTS%'
ORDER BY updated_at DESC LIMIT 15
`).all();
if (nodes.length > 0) {
blocks.push("## Конвенции\n" + nodes.map(n => `- ${n.description}`).join("\n"));
}
}
if (include.includes("hotspots")) {
const rows = db.prepare(`
SELECT cache_key, numeric_value FROM analytics_cache
WHERE category = 'hotspot' AND numeric_value > 0.5
ORDER BY numeric_value DESC LIMIT 5
`).all();
if (rows.length > 0) {
blocks.push("## Проблемные файлы\n" + rows.map(r =>
`- \`${r.cache_key.replace("hotspot:", "")}\` — score: ${r.numeric_value.toFixed(2)}`
).join("\n"));
}
}
if (include.includes("recent_changes")) {
const rows = db.prepare(`
SELECT DISTINCT file_path, COUNT(*) AS cnt
FROM file_edits WHERE recorded_at >= ?
GROUP BY file_path ORDER BY cnt DESC LIMIT 10
`).all(Date.now() - 7 * 24 * 60 * 60 * 1000);
if (rows.length > 0) {
blocks.push("## Недавние изменения\n" + rows.map(r =>
`- \`${r.file_path}\` — ${r.cnt} правок`
).join("\n"));
}
}
if (include.includes("decisions")) {
const rows = db.prepare(`
SELECT title, decision, status FROM decisions
WHERE status = 'accepted' ORDER BY created_at DESC LIMIT 5
`).all();
if (rows.length > 0) {
blocks.push("## Архитектурные решения\n" + rows.map(d =>
`- **${d.title}** [${d.status}]: ${d.decision}`
).join("\n"));
}
}
if (blocks.length === 0) {
throw new ShadowToolError(
"SHADOW_ERR_NO_CONTEXT_AVAILABLE",
"Нет доступного контекста. Плагин только установлен, данных недостаточно."
);
}
return {
injected_at: Date.now(),
sections_count: blocks.length,
context: blocks.join("\n\n"),
focus: args.focus || null,
message: `Контекст проекта внедрён. ${blocks.length} разделов.`,
};
});
},
});
}
// ============================================================
// 6. code_shadow_decide — запись архитектурного решения
// ============================================================
function createDecideTool(ctx: PluginContext, storage: StorageEngine) {
return tool({
description: [
"Записывает архитектурное решение (ADR) в базу Code Shadow.",
"",
"ИСПОЛЬЗУЙ когда принимаешь важное техническое решение.",
"НЕ ИСПОЛЬЗУЙ для мелких решений и фактов о проекте.",
].join("\n"),
args: {
title: tool.schema.string(),
description: tool.schema.string(),
context: tool.schema.string().optional(),
alternatives: tool.schema.array(tool.schema.string()).optional(),
status: tool.schema.enum(["proposed", "accepted", "deprecated", "superseded"]).optional(),
related_files: tool.schema.array(tool.schema.string()).optional(),
tags: tool.schema.array(tool.schema.string()).optional(),
},
async execute(args, context) {
return wrapToolExecute("code_shadow_decide", async () => {
if (args.title.length < 5) {
throw new ShadowToolError(
"SHADOW_ERR_TITLE_TOO_SHORT",
"Заголовок решения должен быть не короче 5 символов.",
{ length: args.title.length }
);
}
if (args.description.length < 20) {
throw new ShadowToolError(
"SHADOW_ERR_DESCRIPTION_TOO_SHORT",
"Описание решения должно быть не короче 20 символов.",
{ length: args.description.length }
);
}
const db = storage.getDb();
const now = Date.now();
// Проверка дубликата
const existing = db.prepare(
"SELECT id FROM decisions WHERE title = ?"
).get(args.title);
if (existing) {
throw new ShadowToolError(
"SHADOW_ERR_DECISION_EXISTS",
`Решение с заголовком «${args.title}» уже существует (ID: ${(existing as { id: number }).id}).`,
{ existing_id: (existing as { id: number }).id }
);
}
const finalTags = args.tags && args.tags.length > 0
? args.tags
: extractTags(`${args.title} ${args.description}`);
const result = db.prepare(`
INSERT INTO decisions (title, context, decision, consequences, status, tags, created_at, updated_at)
VALUES (?, ?, ?, ?, ?, ?, ?, ?)
`).run(
args.title,
args.context || null,
args.description,
null,
args.status || "proposed",
JSON.stringify(finalTags),
now,
now
);
const decisionId = Number(result.lastInsertRowid);
// Связываем с файлами
if (args.related_files && args.related_files.length > 0) {
const decisionNodeId = `decision:${decisionId}`;
db.prepare(`
INSERT OR IGNORE INTO knowledge_nodes (id, type, label, description, metadata, created_at, updated_at)
VALUES (?, 'decision', ?, ?, ?, ?, ?)
`).run(decisionNodeId, args.title, args.description, "{}", now, now);
const insertEdge = db.prepare(`
INSERT OR IGNORE INTO knowledge_edges (source_node_id, target_node_id, relation_type, weight, evidence, created_at)
VALUES (?, ?, 'references', 0.8, 'manual', ?)
`);
for (const file of args.related_files) {
insertEdge.run(decisionNodeId, `file:${file}`, now);
}
}
return {
status: "ok",
decision_id: decisionId,
title: args.title,
status: args.status || "proposed",
tags: finalTags,
related_files_count: args.related_files?.length || 0,
created_at: now,
message: `Архитектурное решение «${args.title}» записано (ID: ${decisionId}).`,
};
});
},
});
}
// ============================================================
// Экспорт: точка входа для PluginRegistration.tools
// ============================================================
export function createAiTools(
ctx: PluginContext,
storage: StorageEngine,
analytics: AnalyticsEngine
): PluginRegistration["tools"] {
return {
code_shadow_analyze: createAnalyzeTool(ctx, storage, analytics),
code_shadow_memory_write: createMemoryWriteTool(ctx, storage),
code_shadow_memory_search: createMemorySearchTool(ctx, storage),
code_shadow_memory_note: createMemoryNoteTool(ctx, storage),
code_shadow_context_inject: createContextInjectTool(ctx, storage, analytics),
code_shadow_decide: createDecideTool(ctx, storage),
};
}| Тулз | Назначение | Ключевые параметры | Magic Context-аналог |
|---|---|---|---|
code_shadow_analyze |
Аналитика кодовой базы (8 режимов) | query, target, timeframe, limit |
нет аналога |
code_shadow_memory_write |
Запись факта в долговременную память | category, content |
ctx_memory |
code_shadow_memory_search |
Поиск по всей памяти Code Shadow | query, sources, limit |
ctx_search |
code_shadow_memory_note |
Создание заметки-напоминания | content, surface_condition |
ctx_note |
code_shadow_context_inject |
Инъекция контекста проекта в текущую сессию | focus, include |
нет аналога |
code_shadow_decide |
Запись архитектурного решения (ADR) | title, description, alternatives |
нет аналога |
// src/tools/utils.ts
import { createHash } from "node:crypto";
/**
* Извлекает теги из текстового содержимого:
* пути к файлам, названия технологий, версии.
*/
export function extractTags(content: string): string[] {
const tags: Set<string> = new Set();
// Пути к файлам
const filePattern = /(?:src|packages|lib|app|tests?|docs)\/[\w\/\-\.]+/gi;
const fileMatches = content.match(filePattern);
fileMatches?.forEach(m => tags.add(`path:${m}`));
// Технологии
const techPattern = /\b(?:react|next\.?js|vue|angular|express|fastify|prisma|drizzle|zod|typebox|typescript|javascript|python|rust|golang|docker|kubernetes|postgres|mysql|redis|sqlite|graphql|rest|grpc)\b/gi;
const techMatches = content.match(techPattern);
techMatches?.forEach(m => tags.add(`tech:${m.toLowerCase()}`));
// Версии
const versionPattern = /(?:>=?|<=?|~|\^)?\d+\.\d+\.\d+/g;
const versionMatches = content.match(versionPattern);
versionMatches?.forEach(m => tags.add(`version:${m}`));
return [...tags].slice(0, 10);
}
/**
* Генерирует короткий хеш содержимого для уникального ID узла.
*/
export function hashContent(content: string): string {
return createHash("sha256")
.update(content)
.digest("hex")
.substring(0, 12);
}
/**
* Парсит строку timeframe в количество дней.
*/
export function parseTimeframe(tf: string): number {
switch (tf) {
case "7d": return 7;
case "30d": return 30;
case "90d": return 90;
case "all": return 365 * 10; // "всё" = 10 лет
default: return 30;
}
}SHADOW_ERR_
├── DATABASE_LOCKED (503, retryable)
├── DATABASE_CORRUPT (500, non-retryable)
├── NOT_INITIALIZED (503, retryable)
├── INSUFFICIENT_DATA (404, retryable)
├── TIMEOUT (504, retryable)
├── INTERNAL (500, non-retryable)
│
├── MISSING_TARGET (400) — не указан target
├── INVALID_QUERY (400) — неизвестный query
├── INVALID_TIMEFRAME (400)
├── INVALID_LIMIT (400)
├── INVALID_SOURCES (400)
├── UNKNOWN_CATEGORY (400)
│
├── FILE_NOT_FOUND (404)
├── NO_HOTSPOTS (404)
├── NO_PREDICTION_DATA (404)
├── NO_DEPENDENCIES (404)
├── EMPTY_KNOWLEDGE_GRAPH (404)
├── NO_DECISIONS (404)
├── NO_CONTEXT_AVAILABLE (404)
├── INVALID_FOCUS (400)
│
├── SEARCH_QUERY_EMPTY (400)
├── SEARCH_TOO_BROAD (400)
├── NO_SOURCES (400)
│
├── CONTENT_TOO_SHORT (400)
├── CONTENT_TOO_LONG (400)
├── DUPLICATE_MEMORY (409)
│
├── TITLE_TOO_SHORT (400)
├── DESCRIPTION_TOO_SHORT (400)
└── DECISION_EXISTS (409)
Конец документа. Версия 1.0.0. Для предложений и правок — PR в github.com/xuviga/code-shadow.