Local branch had diverged from origin/master (sibling commits on the same base). Rewrote local history linearly on top of origin/master, folding in all local content: exa/hot-platform discovery hunters, leaks ledger, nightly sweep orchestrator, updated .gitignore and skill docs, plus the latest hunt output and vault state. Remote-only files (vault keys, channel scripts) were restored rather than dropped, so the resulting tree is a full union of both sides.
1 line
49 KiB
JSON
1 line
49 KiB
JSON
{"repo": "agenthands/helix", "files": {"test/oracle/llm/client.go": {"9dd32f9689bf4716486b52d2f5efcb6c387b9318": {"t": 1785745650.9018784, "n": 4964, "h": "de092c634ebc5082", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n\toai \"github.com/openai/openai-go\"\n\t\"github.com/openai/openai-go/option\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvDeepSeekKey is the environment variable for the DeepSeek API key.\n\tEnvDeepSeekKey = \"DEEPSEEK_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"HELIX_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"HELIX_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"HELIX_INLINE_JUDGE\"\n\t// EnvLLMProvider selects the LLM provider: \"anthropic\" (default) or \"deepseek\".\n\tEnvLLMProvider = \"HELIX_LLM_PROVIDER\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-5-20251001\"\n\t// DefaultDeepSeekModel is the default DeepSeek model.\n\tDefaultDeepSeekModel = \"deepseek-chat\"\n)\n\n// Provider returns the active LLM provider name.\nfunc Provider() string {\n\tif p := os.Getenv(EnvLLMProvider); p != \"\" {\n\t\treturn p\n\t}\n\t// Auto-detect: if DeepSeek key is set but Anthropic isn't, use DeepSeek.\n\tif os.Getenv(EnvAPIKey) == \"\" && os.Getenv(EnvDeepSeekKey) != \"\" {\n\t\treturn \"deepseek\"\n\t}\n\treturn \"anthropic\"\n}\n\n// SkipWithoutAPIKey skips the test if no LLM API key is available (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tp := Provider()\n\tswitch p {\n\tcase \"deepseek\":\n\t\tif os.Getenv(EnvDeepSeekKey) == \"\" {\n\t\t\tt.Skip(\"DEEPSEEK_API_KEY not set\")\n\t\t}\n\tdefault:\n\t\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t\t}\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\n// Only used when provider is \"anthropic\".\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses HELIX_TEST_MODEL if set, otherwise defaults per provider (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\tif Provider() == \"deepseek\" {\n\t\treturn DefaultDeepSeekModel\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses HELIX_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Routes to the active provider.\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tif Provider() == \"deepseek\" {\n\t\treturn askDeepSeek(ctx, model, system, userPrompt)\n\t}\n\treturn askAnthropic(ctx, client, model, system, userPrompt)\n}\n\n// askAnthropic sends via Anthropic API. Uses MaxTokens: 1024 (D-01).\nfunc askAnthropic(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (string, string, error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// askDeepSeek sends via DeepSeek's OpenAI-compatible API.\nfunc askDeepSeek(ctx context.Context, model, system, userPrompt string) (string, string, error) {\n\tclient := oai.NewClient(\n\t\toption.WithAPIKey(os.Getenv(EnvDeepSeekKey)),\n\t\toption.WithBaseURL(\"https://api.deepseek.com\"),\n\t)\n\n\tvar messages []oai.ChatCompletionMessageParamUnion\n\tif system != \"\" {\n\t\tmessages = append(messages, oai.SystemMessage(system))\n\t}\n\tmessages = append(messages, oai.UserMessage(userPrompt))\n\n\tresp, err := client.Chat.Completions.New(ctx, oai.ChatCompletionNewParams{\n\t\tModel: model,\n\t\tMessages: messages,\n\t})\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"deepseek API call failed: %w\", err)\n\t}\n\n\tif len(resp.Choices) == 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no choices in deepseek response\")\n\t}\n\n\tchoice := resp.Choices[0]\n\treturn choice.Message.Content, string(choice.FinishReason), nil\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}, "6501092f14b36d1e467bb71a2ae21db2a850acd3": {"t": 1785755391.6387677, "n": 4964, "h": "de092c634ebc5082", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n\toai \"github.com/openai/openai-go\"\n\t\"github.com/openai/openai-go/option\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvDeepSeekKey is the environment variable for the DeepSeek API key.\n\tEnvDeepSeekKey = \"DEEPSEEK_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"HELIX_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"HELIX_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"HELIX_INLINE_JUDGE\"\n\t// EnvLLMProvider selects the LLM provider: \"anthropic\" (default) or \"deepseek\".\n\tEnvLLMProvider = \"HELIX_LLM_PROVIDER\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-5-20251001\"\n\t// DefaultDeepSeekModel is the default DeepSeek model.\n\tDefaultDeepSeekModel = \"deepseek-chat\"\n)\n\n// Provider returns the active LLM provider name.\nfunc Provider() string {\n\tif p := os.Getenv(EnvLLMProvider); p != \"\" {\n\t\treturn p\n\t}\n\t// Auto-detect: if DeepSeek key is set but Anthropic isn't, use DeepSeek.\n\tif os.Getenv(EnvAPIKey) == \"\" && os.Getenv(EnvDeepSeekKey) != \"\" {\n\t\treturn \"deepseek\"\n\t}\n\treturn \"anthropic\"\n}\n\n// SkipWithoutAPIKey skips the test if no LLM API key is available (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tp := Provider()\n\tswitch p {\n\tcase \"deepseek\":\n\t\tif os.Getenv(EnvDeepSeekKey) == \"\" {\n\t\t\tt.Skip(\"DEEPSEEK_API_KEY not set\")\n\t\t}\n\tdefault:\n\t\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t\t}\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\n// Only used when provider is \"anthropic\".\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses HELIX_TEST_MODEL if set, otherwise defaults per provider (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\tif Provider() == \"deepseek\" {\n\t\treturn DefaultDeepSeekModel\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses HELIX_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Routes to the active provider.\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tif Provider() == \"deepseek\" {\n\t\treturn askDeepSeek(ctx, model, system, userPrompt)\n\t}\n\treturn askAnthropic(ctx, client, model, system, userPrompt)\n}\n\n// askAnthropic sends via Anthropic API. Uses MaxTokens: 1024 (D-01).\nfunc askAnthropic(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (string, string, error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// askDeepSeek sends via DeepSeek's OpenAI-compatible API.\nfunc askDeepSeek(ctx context.Context, model, system, userPrompt string) (string, string, error) {\n\tclient := oai.NewClient(\n\t\toption.WithAPIKey(os.Getenv(EnvDeepSeekKey)),\n\t\toption.WithBaseURL(\"https://api.deepseek.com\"),\n\t)\n\n\tvar messages []oai.ChatCompletionMessageParamUnion\n\tif system != \"\" {\n\t\tmessages = append(messages, oai.SystemMessage(system))\n\t}\n\tmessages = append(messages, oai.UserMessage(userPrompt))\n\n\tresp, err := client.Chat.Completions.New(ctx, oai.ChatCompletionNewParams{\n\t\tModel: model,\n\t\tMessages: messages,\n\t})\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"deepseek API call failed: %w\", err)\n\t}\n\n\tif len(resp.Choices) == 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no choices in deepseek response\")\n\t}\n\n\tchoice := resp.Choices[0]\n\treturn choice.Message.Content, string(choice.FinishReason), nil\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}, "95c0025c346d6e20b1e0b99cfab075f6db5cc991": {"t": 1785755392.8431807, "n": 4970, "h": "7b138b582c2c0009", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n\toai \"github.com/openai/openai-go\"\n\t\"github.com/openai/openai-go/option\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvDeepSeekKey is the environment variable for the DeepSeek API key.\n\tEnvDeepSeekKey = \"DEEPSEEK_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"SERENA_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"SERENA_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"SERENA_INLINE_JUDGE\"\n\t// EnvLLMProvider selects the LLM provider: \"anthropic\" (default) or \"deepseek\".\n\tEnvLLMProvider = \"SERENA_LLM_PROVIDER\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-5-20251001\"\n\t// DefaultDeepSeekModel is the default DeepSeek model.\n\tDefaultDeepSeekModel = \"deepseek-chat\"\n)\n\n// Provider returns the active LLM provider name.\nfunc Provider() string {\n\tif p := os.Getenv(EnvLLMProvider); p != \"\" {\n\t\treturn p\n\t}\n\t// Auto-detect: if DeepSeek key is set but Anthropic isn't, use DeepSeek.\n\tif os.Getenv(EnvAPIKey) == \"\" && os.Getenv(EnvDeepSeekKey) != \"\" {\n\t\treturn \"deepseek\"\n\t}\n\treturn \"anthropic\"\n}\n\n// SkipWithoutAPIKey skips the test if no LLM API key is available (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tp := Provider()\n\tswitch p {\n\tcase \"deepseek\":\n\t\tif os.Getenv(EnvDeepSeekKey) == \"\" {\n\t\t\tt.Skip(\"DEEPSEEK_API_KEY not set\")\n\t\t}\n\tdefault:\n\t\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t\t}\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\n// Only used when provider is \"anthropic\".\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses SERENA_TEST_MODEL if set, otherwise defaults per provider (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\tif Provider() == \"deepseek\" {\n\t\treturn DefaultDeepSeekModel\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses SERENA_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Routes to the active provider.\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tif Provider() == \"deepseek\" {\n\t\treturn askDeepSeek(ctx, model, system, userPrompt)\n\t}\n\treturn askAnthropic(ctx, client, model, system, userPrompt)\n}\n\n// askAnthropic sends via Anthropic API. Uses MaxTokens: 1024 (D-01).\nfunc askAnthropic(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (string, string, error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// askDeepSeek sends via DeepSeek's OpenAI-compatible API.\nfunc askDeepSeek(ctx context.Context, model, system, userPrompt string) (string, string, error) {\n\tclient := oai.NewClient(\n\t\toption.WithAPIKey(os.Getenv(EnvDeepSeekKey)),\n\t\toption.WithBaseURL(\"https://api.deepseek.com\"),\n\t)\n\n\tvar messages []oai.ChatCompletionMessageParamUnion\n\tif system != \"\" {\n\t\tmessages = append(messages, oai.SystemMessage(system))\n\t}\n\tmessages = append(messages, oai.UserMessage(userPrompt))\n\n\tresp, err := client.Chat.Completions.New(ctx, oai.ChatCompletionNewParams{\n\t\tModel: model,\n\t\tMessages: messages,\n\t})\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"deepseek API call failed: %w\", err)\n\t}\n\n\tif len(resp.Choices) == 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no choices in deepseek response\")\n\t}\n\n\tchoice := resp.Choices[0]\n\treturn choice.Message.Content, string(choice.FinishReason), nil\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}, "49b599317bdb461e4c8a0fbd4fd7eb0a45d5bd5f": {"t": 1785755393.9997447, "n": 4968, "h": "688776e9b5332fcc", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n\toai \"github.com/openai/openai-go\"\n\t\"github.com/openai/openai-go/option\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvDeepSeekKey is the environment variable for the DeepSeek API key.\n\tEnvDeepSeekKey = \"DEEPSEEK_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"SERENA_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"SERENA_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"SERENA_INLINE_JUDGE\"\n\t// EnvLLMProvider selects the LLM provider: \"anthropic\" (default) or \"deepseek\".\n\tEnvLLMProvider = \"SERENA_LLM_PROVIDER\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-20250414\"\n\t// DefaultDeepSeekModel is the default DeepSeek model.\n\tDefaultDeepSeekModel = \"deepseek-chat\"\n)\n\n// Provider returns the active LLM provider name.\nfunc Provider() string {\n\tif p := os.Getenv(EnvLLMProvider); p != \"\" {\n\t\treturn p\n\t}\n\t// Auto-detect: if DeepSeek key is set but Anthropic isn't, use DeepSeek.\n\tif os.Getenv(EnvAPIKey) == \"\" && os.Getenv(EnvDeepSeekKey) != \"\" {\n\t\treturn \"deepseek\"\n\t}\n\treturn \"anthropic\"\n}\n\n// SkipWithoutAPIKey skips the test if no LLM API key is available (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tp := Provider()\n\tswitch p {\n\tcase \"deepseek\":\n\t\tif os.Getenv(EnvDeepSeekKey) == \"\" {\n\t\t\tt.Skip(\"DEEPSEEK_API_KEY not set\")\n\t\t}\n\tdefault:\n\t\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t\t}\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\n// Only used when provider is \"anthropic\".\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses SERENA_TEST_MODEL if set, otherwise defaults per provider (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\tif Provider() == \"deepseek\" {\n\t\treturn DefaultDeepSeekModel\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses SERENA_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Routes to the active provider.\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tif Provider() == \"deepseek\" {\n\t\treturn askDeepSeek(ctx, model, system, userPrompt)\n\t}\n\treturn askAnthropic(ctx, client, model, system, userPrompt)\n}\n\n// askAnthropic sends via Anthropic API. Uses MaxTokens: 1024 (D-01).\nfunc askAnthropic(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (string, string, error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// askDeepSeek sends via DeepSeek's OpenAI-compatible API.\nfunc askDeepSeek(ctx context.Context, model, system, userPrompt string) (string, string, error) {\n\tclient := oai.NewClient(\n\t\toption.WithAPIKey(os.Getenv(EnvDeepSeekKey)),\n\t\toption.WithBaseURL(\"https://api.deepseek.com\"),\n\t)\n\n\tvar messages []oai.ChatCompletionMessageParamUnion\n\tif system != \"\" {\n\t\tmessages = append(messages, oai.SystemMessage(system))\n\t}\n\tmessages = append(messages, oai.UserMessage(userPrompt))\n\n\tresp, err := client.Chat.Completions.New(ctx, oai.ChatCompletionNewParams{\n\t\tModel: model,\n\t\tMessages: messages,\n\t})\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"deepseek API call failed: %w\", err)\n\t}\n\n\tif len(resp.Choices) == 0 {\n\t\treturn \"\", \"\", fmt.Errorf(\"no choices in deepseek response\")\n\t}\n\n\tchoice := resp.Choices[0]\n\treturn choice.Message.Content, string(choice.FinishReason), nil\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}, "c4a7271ffc2a563b769dcf3bd46aa21c43ef17a2": {"t": 1785755396.3486178, "n": 2830, "h": "5621b7dd4980a2a1", "c": "//go:build llm || llmjudge\n\npackage llm\n\nimport (\n\t\"context\"\n\t\"fmt\"\n\t\"os\"\n\t\"testing\"\n\t\"time\"\n\n\t\"github.com/anthropics/anthropic-sdk-go\"\n)\n\nconst (\n\t// EnvAPIKey is the environment variable for the Anthropic API key.\n\tEnvAPIKey = \"ANTHROPIC_API_KEY\"\n\t// EnvTestModel overrides the subject model for behavioral tests.\n\tEnvTestModel = \"SERENA_TEST_MODEL\"\n\t// EnvJudgeModel overrides the judge model for scoring.\n\tEnvJudgeModel = \"SERENA_JUDGE_MODEL\"\n\t// EnvInlineJudge enables inline judge mode when set to \"1\".\n\tEnvInlineJudge = \"SERENA_INLINE_JUDGE\"\n\t// DefaultSubjectModel is the cheapest Claude model for behavioral tests (D-11).\n\tDefaultSubjectModel = \"claude-haiku-4-20250414\"\n)\n\n// SkipWithoutAPIKey skips the test if ANTHROPIC_API_KEY is not set (D-18).\n// Never logs the key value to avoid information disclosure (T-21-01).\nfunc SkipWithoutAPIKey(t *testing.T) {\n\tt.Helper()\n\tif os.Getenv(EnvAPIKey) == \"\" {\n\t\tt.Skip(\"ANTHROPIC_API_KEY not set\")\n\t}\n}\n\n// NewClient creates an Anthropic client that reads ANTHROPIC_API_KEY from env.\nfunc NewClient() anthropic.Client {\n\treturn anthropic.NewClient()\n}\n\n// SubjectModel returns the model ID for behavioral tests.\n// Uses SERENA_TEST_MODEL if set, otherwise DefaultSubjectModel (D-10/D-11).\nfunc SubjectModel() string {\n\tif m := os.Getenv(EnvTestModel); m != \"\" {\n\t\treturn m\n\t}\n\treturn DefaultSubjectModel\n}\n\n// JudgeModel returns the model ID for judge scoring and whether the run is self-judged.\n// Uses SERENA_JUDGE_MODEL if set, otherwise falls back to SubjectModel (D-10/D-12).\nfunc JudgeModel() (model string, selfJudged bool) {\n\tif m := os.Getenv(EnvJudgeModel); m != \"\" {\n\t\treturn m, false\n\t}\n\treturn SubjectModel(), true\n}\n\n// AskSingleTurn sends a single-turn prompt and returns the response text,\n// stop reason, and any error. Uses MaxTokens: 1024 (D-01).\nfunc AskSingleTurn(ctx context.Context, client anthropic.Client, model, system, userPrompt string) (responseText string, stopReason string, err error) {\n\tparams := anthropic.MessageNewParams{\n\t\tModel: model,\n\t\tMaxTokens: 1024,\n\t\tMessages: []anthropic.MessageParam{\n\t\t\tanthropic.NewUserMessage(anthropic.NewTextBlock(userPrompt)),\n\t\t},\n\t}\n\tif system != \"\" {\n\t\tparams.System = []anthropic.TextBlockParam{\n\t\t\t{Text: system},\n\t\t}\n\t}\n\n\tmessage, err := client.Messages.New(ctx, params)\n\tif err != nil {\n\t\treturn \"\", \"\", fmt.Errorf(\"anthropic API call failed: %w\", err)\n\t}\n\n\t// Extract text from first TextBlock in response content.\n\tfor _, block := range message.Content {\n\t\tif v, ok := block.AsAny().(anthropic.TextBlock); ok {\n\t\t\treturn v.Text, string(message.StopReason), nil\n\t\t}\n\t}\n\n\treturn \"\", string(message.StopReason), fmt.Errorf(\"no text content in response\")\n}\n\n// InterCallDelay sleeps 250ms between API calls to avoid rate limits (D-19).\nfunc InterCallDelay() {\n\ttime.Sleep(250 * time.Millisecond)\n}\n"}}, ".planning/milestones/v1.10-research/SUMMARY.md": {"9dd32f9689bf4716486b52d2f5efcb6c387b9318": {"t": 1786068555.470047, "n": 24315, "h": "99f171ae602d3369", "c": "# Project Research Summary — Helix v1.10 Live Semantic Index\n\n**Domain:** Live, evidence-backed semantic graph layered onto a shipped LSP-backed code intelligence daemon (DuckDB committed snapshots ⊕ live overlay ⊕ LSP validation ⊕ agent-facing context tools ⊕ guardrails ⊕ eval harness)\n**Researched:** 2026-05-03\n**Confidence:** HIGH overall — STACK and ARCHITECTURE anchored in v1.9 source, FEATURES in adjacent-product docs, PITFALLS in shipped Helix bugs and upstream issues. MEDIUM on guardrail-receipt schemas (genuinely novel) and dynamic-language type-resolution conventions.\n\n## Executive Summary\n\nHelix v1.10 turns the v1.9 online LSP/tree-sitter capabilities into a durable, live, evidence-backed semantic graph. The decision-defining shape is **DuckDB committed snapshots + fsnotify live overlay + effective-read semantics + LSP revalidation queue**, exposed to agents as 10 new MCP tools, gated by a guardrail/receipt policy engine, and validated end-to-end by an out-of-process eval harness comparing four modes (`baseline / native / semantic / semantic_guarded`). The adjacent-product survey shows this combination is **not** the dominant pattern in any shipping product: SCIP/Glean/Stack-Graphs ship batch graphs without live overlay; Aider/Cursor/Cody ship live retrieval without a durable graph. **Freshness-as-API** (every response carries `freshness` + `score_status` + `pending_lsp_files`) and **semantic safety receipts** (destructive edits gated on prior `find_references`/`analyze_blast_radius` evidence) are genuine differentiators with no published prior art.\n\nThe recommended approach lands in 13 phases under a clean dependency order: store first → tree-sitter extraction + stable symbol IDs → live overlay → LSP enrichment → graph scores → clustering → 10 MCP tools → existing-tool integration via strangler fig → compaction → type resolution → guardrails → eval. New code lives in **Layer 1.5** (`internal/semantic/`, `internal/phasegraph/`, `internal/guardrails/`, `internal/eval/`), between the kernel and skills, with strict acyclic imports. Integration with kernel-edit and repomap is via injected callbacks (`postEditHook`, `SetSemanticLookup`) so the kernel never imports semantic. The DuckDB binding is `github.com/duckdb/duckdb-go v2.10502.0` (CGO=1 required, single-binary preserved via bundled static libs); the v1.9 Phase 51.1 stub policy extends to auto-disable `semantic_index.enabled` on CGO=0 builds.\n\nTop risks concentrate in **state coherence under concurrency** (overlay × graph-cache × compaction race; fsnotify dropping watches on editor atomic-rename; PageRank score instability while LSP enrichment lands asynchronously) and **agent-shaped failure modes** (receipts lost to context compaction, eval prompt leakage to commercial LLMs). Mitigations are concrete and architectural: epoch-CAS contract between overlay writes and compaction, directory-watching with content-hash scrub, `graph_version` + monotone-frontier rule on tool envelopes, server-side receipt store with ID-only forwarding. **Must-not-regress invariants from v1.9** — `LazyInit`-last middleware order, single canonical `GrammarRegistry`, structured LS readiness gates (rust-analyzer `experimental/serverStatus`, jdtls `WaitUntilJavaReady`), bounded-label metrics with no source content — all carry forward and are explicitly preserved.\n\n## Key Findings\n\n### Recommended Stack (additions only; v1.9 stack fixed)\n\n- **`github.com/duckdb/duckdb-go` v2.10502.0** (DuckDB 1.5.2): fact-store driver, official post-donation repo, `database/sql` + Appender, bundled static libs cover the 6-archive matrix. **CGO=1 required** — extends Phase 51.1 stub policy.\n- **`gonum.org/v1/gonum` v0.16.0 test-only**: oracle for hand-rolled PageRank/components; never imported from production per ADR-005.\n- **`github.com/tiktoken-go/tokenizer` v0.6.x**: pure-Go embedded vocab; rejected `pkoukk/tiktoken-go` because it network-downloads vocab.\n- **`github.com/bluekeyes/go-gitdiff` v0.8.x**: pure-Go patch parse + apply; rejected `sourcegraph/go-diff` as parser-only.\n- **`fsnotify` v1.9.0** (already vendored): kept; build recursive walker, atomic-rename re-attach, `ENOSPC` degraded-mode in-tree.\n- **Pipeline DAG**: stdlib only (~80 LOC Kahn's algorithm).\n- **Type resolution / fixpoint**: stdlib only; comment parsers regex-tractable.\n\n**CGO posture (load-bearing):** `semantic_index.enabled=true` requires CGO=1. CGO=0 builds compile via `//go:build !cgo` stubs that auto-return `Kind: Unsupported`. Daemon must auto-set `cfg.SemanticIndex.Enabled = false` when `treesitter.Available == false`. **What NOT to pull in:** any KG/RDF stack, embedding/vector libs, heavyweight DAG/workflow engines, gonum-in-production, recursive-watch wrappers.\n\n### Expected Features\n\n**Must have (table stakes):** durable queryable symbol+reference graph with stable IDs across renames; incremental update without full reindex; cross-file find-references / call-hierarchy; token-budgeted ranked context; tree-sitter-first extraction with LSP enrichment; PageRank importance ranking; indexing-progress + status; diagnostics integration after edits.\n\n**Should have (differentiators):**\n- Effective-read semantics (snapshot ⊕ overlay) — addresses Cursor's top complaint\n- First-class freshness on every response\n- Tiered confidence on edges with explicit evidence kinds (1.00 LSP / 0.90 annotation / … / 0.20 unknown) + `validation_state`\n- Live LSP revalidation queue with priority boosts on edit\n- Multi-projection PageRank with per-projection freshness\n- Cluster maps + cluster explanations\n- Guardrail policy engine + safety receipts (semantic correlation across tool calls; novel)\n- Eval harness with 4 modes\n- Type resolution with bounded fixpoint for dynamic languages (capped, comment-fallback ≤ 0.60)\n- `validate_graph_edge` (agent challenges the index)\n- Typed pipeline DAG with phase validation at startup\n\n**Defer (v1.10.x / v1.11+):** multi-projection PageRank, clustering tools, P1 retrieval companions (`find_related_symbols` / `get_change_impact_graph` / `explain_symbol_deep` / `validate_graph_edge`), `semantic_guarded` mode + G-006..G-010, type resolution + access chains, comment-based fallback, idle compaction tuning, Java/Rust first-class extraction, cross-repo graphs, v1.9 carry-over (PKG-01 SC-3, distros, Phase 51 reproducibility scope, Phase 55 forwarder span).\n\n**Anti-features (refusals):** vector/embedding search; RDF/SPARQL KG; \"real-time across whole repo\" marketing; auto-execute when guardrails pass; in-process LSP servers; soundness-grade type inference; SWE-bench leaderboard chase; LLM-generated edge explanations; persistent receipts detached from `graph_version`; full-rebuild on every Helix start.\n\n### Architecture Approach\n\n**Layer 1.5 placement** between kernel and skills. Strict acyclic imports — kernel never depends on semantic; coupling inverted via callbacks.\n\n**Major components:**\n1. **Semantic Service** owns `*sql.DB` for DuckDB at `<workspace>/.helix/semantic.duckdb`; opens at new daemon bootstrap step **2.5** between language registry and kernel creation; disable path returns `nil`, all callsites guard.\n2. **Live Update Pipeline** — fsnotify directory-watcher with atomic-rename re-attach + content-hash scrub; coalescer (250ms debounce, 200-event bulk threshold); overlay writer with `overlay_epoch`; idle compaction goroutine.\n3. **LSP Enrichment Worker** — single goroutine with `container/heap` priority queue; reuses `kernel.Pool().AcquireLease(...)` via small `LeaseAcquirer` interface (no kernel import); honors v1.9 readiness gates.\n4. **Graph + Rank + Cluster** — in-memory cache loaded from DuckDB; hand-rolled multi-projection weighted PageRank with personalized restart and incremental local repair.\n5. **MCP Tools + Skill** — 10 new tools registered via existing skill adapter pattern (new daemon step 13.5).\n6. **Existing-Tool Integration (strangler fig)** — `repomapSkill.SetSemanticLookup(lookup)` mirrors `SetEnrichFn`; `get_repo_map` / `get_context` / `analyze_blast_radius` / `get_health` consult semantic when available, fall back to v1.9. Zero source change to `internal/repomap` engine.\n7. **Edit-Tool Hook (callback inversion)** — `edit.RegisterTools` gains `postEditHook func(ctx, []string, source)`; daemon wires `d.semantic.LiveQueue().EnqueueChangeEventsFunc()`. Threaded through `fileops.replace_in_file` / `fuzzy_edit` too. `nil`-safe.\n8. **Guardrail Middleware** — installed at new step **14b.5** between Suggestion and LazyInit. Execution order: `LazyInit → Guardrail → Suggestion → ProfileFilter → Telemetry → handler`. **LazyInit MUST remain installed last (executes first).** Telemetry gains `guardrail_blocked` / `guardrail_warned` outcome classes.\n9. **Pipeline DAG** — strangler fig: stdlib library used immediately for new graphs (semantic indexing, live update, eval); imperative `daemon.New` bootstrap stays with `// TODO(v1.11): migrate` marker.\n10. **Eval Harness** — out-of-process by default; subprocess `helix daemon` per task with isolated config; agent (Anthropic / DeepSeek refactored from `test/oracle/llm/`) over stdio forwarder; in-process variant for `make eval-quick`. Each mode is a config preset. New `--profile=baseline` strips Helix tools entirely.\n\n**Shutdown ordering:** semantic.Run under daemon errgroup; on cancel: stop accepting events → drain coalescer → drain LSP queue → flush overlay (no compaction) → close DuckDB → kernel shuts down LS workers (v1.9 ordering preserved last).\n\n**DuckDB cross-process rule:** all store access via daemon. CLI subcommands and eval harness MUST NOT reopen the `.duckdb` file (DuckDB single-writer). Lock with vet rule forbidding `duckdb-go` import outside `internal/semantic/store/`.\n\n### Critical Pitfalls (top 5; full set in PITFALLS.md)\n\n1. **C1 — Overlay × graph-cache × compaction race.** Three writers, three independent monotonic clocks, no single linearization point; ClearOverlay can drop overlay rows committed after MergeBaseAndOverlay. **Mitigation:** epoch-CAS — compaction snapshots `overlay_epoch`, ClearOverlay deletes rows ≤ captured epoch. Property test in P8.\n2. **C2 — fsnotify silent watcher death on atomic-rename.** Vim/JetBrains/VS Code save patterns kill inode-attached watchers; Linux inotify per-user limit blows up on >8k-dir repos. **Mitigation:** watch directories not files; eager re-attach on RENAME/REMOVE; required (not best-effort) 5-min content-hash scrub; ENOSPC → manifest-poll fallback per workspace; surface in `get_semantic_graph_status`.\n3. **C3 — PageRank score instability while LSP enrichment lands.** Async confidence promotion (0.4 → 1.0) re-ranks the graph between calls. **Mitigation:** every ranked envelope returns `graph_version` + `enrichment_level`; default to last-committed-snapshot scores; bound incremental repair frontier (`max_repair_nodes`, fail-closed); determinism test (NodeID-sorted tiebreak).\n4. **C8 — LSP enrichment death spiral.** Post-`git checkout` flood buries jdtls (just stabilized in Phase 56); interactive p95 climbs as Helix indexes more. **Mitigation:** revalidation queue strictly lower priority via separate token bucket + concurrency cap (default 1 worker); interactive deadline preempts; do NOT flood after `bulk_update` (lazy-on-tool-call drives enrichment); honor v1.9 readiness gates before enqueueing.\n5. **C7 — Stable symbol ID drift on rename / generics / overloads / anonymous symbols.** Breaks `analyze_blast_radius`, `get_change_impact_graph`, `find_related_symbols`. **Mitigation:** key contract `repo_id || package_path || enclosing_chain || name || arity || receiver_type` — explicitly NOT line/column, NOT `signature_hash`, NOT generic type-args. AST-shape hash for anonymous. 30+ before/after test matrix per language. Lock contract in P1.\n\n**Other notable risks:** C5 cross-process DuckDB lock (architectural rule + vet lint); C6 DuckDB JSON growth (CHECKPOINT + weekly VACUUM, evidence size cap); M1 overlay rows lost in compaction crash (single-tx OR `compaction_journal`); M4 receipts lost to context compaction (server-side receipt store, ID-only forwarding); M5 eval prompt leakage (synthetic-only corpora, retention-zero); M6 metrics cardinality (extend v1.2 bounded-label CI lint); M7 fallback path drift (`source` field in every envelope; index-disabled goldens).\n\n### Must-Not-Regress Invariants (v1.9 carryover)\n\n- **Middleware install LIFO order:** `LazyInit` MUST remain installed last (executes first). Guardrail inserts at 14b.5 between Suggestion and LazyInit.\n- **Single canonical `GrammarRegistry`** (BUG-04, Phase 49) injected from daemon bootstrap; semantic extractors consume the same instance.\n- **Structured LS readiness gates** (BUG-02 rust-analyzer `experimental/serverStatus`, Phase 56 jdtls `JdtlsAdapter.WaitUntilJavaReady`, `Worker.Start` `OnNotification` wiring) — semantic LSP enrichment honors these.\n- **Bounded-label metrics with no source content** in metrics or traces — extend v1.2 cardinality allowlist; PromQL validator (registry-driven, fail-closed) covers new families.\n- **Single-binary + CGO=0 stub policy** — daemon refuses semantic with `Kind: Unsupported` and remediation text.\n\n## Implications for Roadmap\n\n13 phases honoring the dependency order from ARCHITECTURE.md \"Suggested build order\". Deviations from SPEC §32: pipeline DAG library split out as P0.5 so P1/P2/P10 can consume it; type resolution (P11) moved between P8 and P9 (improves edge precision; doesn't gate other phases).\n\n### Phase P0: Schema & Store + Config + Cardinality\n**Rationale:** Foundation; locks cross-process access rule (C5), JSON-vs-typed-column discipline (C6), cardinality allowlist (M6) before any consumer depends on the store.\n**Delivers:** `internal/semantic/{store,types,config}`; DuckDB schema; schema versioning; `semantic_index.enabled` config + 4-layer precedence; vet rule forbidding `duckdb-go` outside `internal/semantic/store/`; CGO=0 stub.\n**Avoids:** C5, C6, M5, M6.\n\n### Phase P0.5: Pipeline DAG Library\n**Rationale:** Consumed by P1/P2/P10 planners. Cheap (~80 LOC stdlib).\n**Delivers:** `internal/phasegraph/`.\n**Avoids:** m1 half-migration (bootstrap stays imperative with TODO(v1.11) marker).\n\n### Phase P1: Tree-sitter Extraction + Stable Symbol IDs\n**Rationale:** Symbol identity is the foundational graph contract — must lock before live overlay or any consumer.\n**Delivers:** `internal/semantic/{extract,resolve}`; Go / TS+JS / Python first-class extraction; key contract; 30+ before/after test matrix per language.\n**Avoids:** C7.\n**Research flag:** language-specific edge cases (Go generics, TS overloads, Python decorators).\n\n### Phase P2: Live Overlay + Watcher\n**Rationale:** First daemon integration; introduces `postEditHook`. Must define epoch contract before P8 depends on it.\n**Delivers:** `internal/semantic/live/*`; directory-watcher + atomic-rename re-attach + ENOSPC fallback + scrub; coalescer; overlay writer with `overlay_epoch`; `postEditHook` wired into `edit.RegisterTools` and `fileops.RegisterTools`; daemon bootstrap step 9.5.\n**Avoids:** C1 race, C2 watcher misses, M2 bulk threshold.\n**Research flag:** fsnotify edge cases across editors require concrete fixture validation.\n\n### Phase P3: LSP Enrichment Worker\n**Rationale:** Depends on lspool lease API; consumes v1.9 readiness gates.\n**Delivers:** `internal/semantic/lspenrich/*`; priority queue; `LeaseAcquirer` interface (no kernel import); concurrency cap; readiness-gate integration.\n**Avoids:** C8 death spiral; preserves BUG-02 / Phase 56 invariants.\n\n### Phase P4: Graph Scores (single-projection MVP)\n**Rationale:** Reuses v1.6 RepoMap PageRank; multi-projection deferred.\n**Delivers:** `internal/semantic/{graph,rank}/*`; weighted PageRank with shared `applyDanglingMass`; `graph_version`; bounded incremental repair; determinism test.\n**Avoids:** C3, C4.\n\n### Phase P5: Clustering (defer to v1.10.x)\n**Delivers:** `internal/semantic/cluster/*`; cluster snapshot keyed on `graph_version`.\n**Avoids:** C3 (cluster drift mirrors score drift).\n\n### Phase P6: 10 New MCP Tools\n**Rationale:** Existing skill registration pattern; `get_semantic_context` is the headline tool.\n**Delivers:** `internal/semantic/tools/*` + `internal/skill/semantic/*`; P0 set of 4 (`index_semantic_graph`, `refresh_semantic_graph`, `get_semantic_graph_status`, `get_semantic_context`); remaining 6 in v1.10.x; `freshness` field on every response; stable-key tiebreak.\n**Avoids:** M8 non-determinism.\n\n### Phase P7: Existing-Tool Integration (strangler fig)\n**Rationale:** Lowest-risk integration — zero source change to `internal/repomap` engine.\n**Delivers:** `repomapSkill.SetSemanticLookup(lookup)`; `get_repo_map` / `get_context` / `analyze_blast_radius` / `get_health` consult semantic-when-available with automatic v1.9 fallback; `source` field in result envelopes; index-disabled goldens preserved.\n**Avoids:** M7.\n\n### Phase P8: Compaction & Retention\n**Rationale:** Honors P2 epoch contract.\n**Delivers:** idle-debounced compaction; `CHECKPOINT` end-of-compact; weekly `VACUUM` (config-gated); single-tx commit OR `compaction_journal`; long-repo bench fixture.\n**Avoids:** C1 (epoch CAS), M1, C6.\n\n### Phase P11: Type Resolution (out-of-order, between P8 and P9)\n**Rationale:** Improves edge precision; doesn't gate other phases. Comment-fallback work load-bearing for JS/Python eval signal.\n**Delivers:** `internal/semantic/typeresolve/*`; bounded fixpoint with hard iteration cap (`max_fixpoint_iters=8`) + early-exit on no progress; conservative emission on non-convergence; comment-derived evidence capped at 0.7.\n**Avoids:** M3, m3.\n**Research flag:** comment-format grammar subset (JSDoc / PHPDoc / YARD / Python typing comments).\n\n### Phase P9: Guardrails + Middleware\n**Rationale:** Depends on graph + receipts; must come after graph scores so policy can read freshness/`score_status`.\n**Delivers:** `internal/guardrails/*`; `internal/mcp/guardrail_middleware.go`; daemon step 14b.5; G-001..G-005 with `warn` default; server-side receipt store (5-min TTL + `graph_version` invalidation); ID-only forwarding; destructive tools look up claims server-side; telemetry outcomes; `DoD.md` + `GUARDRAILS.md`.\n**Avoids:** M4.\n**Research flag:** receipt-schema design has no published prior art.\n\n### Phase P10: Eval Harness\n**Rationale:** Depends on a working semantic stack + guardrails.\n**Delivers:** `internal/eval/*` + `internal/cli/eval/*`; subprocess daemon orchestration with isolated config dirs; agent over stdio forwarder; 4 modes; cross-model judging (informational, never CI gate); synthetic-only corpora + OSS Helix repo; retention-zero provider config; `make eval-quick` in-process.\n**Avoids:** M5, m4.\n**Research flag:** task-class DoD definitions and tool-behavior scoring rubric calibration; provider data-retention TOS re-verification.\n\n### Phase P12: Pipeline DAG bootstrap migration → v1.11\nv1.10 lands `phasegraph` library at P0.5 and uses it for new graphs only. Migrating imperative `daemon.New` 16-step bootstrap touches every test that constructs a daemon; deferred per strangler fig.\n\n### v1.9 Carry-over (folded into v1.10)\n\n- **PKG-01 SC-3** — cut first signed release with real minisign keypair (deployment-gated; engineering-side checks already pass).\n- **PKG-DEFER-03/04/05** — Homebrew tap, Scoop bucket, native Linux package; re-scope based on first signed release feedback.\n- **Phase 51 reproducibility-gate architectural fix** — real-release-vs-Pass-3 OR CONTRIBUTING.md wording softening.\n- **Phase 55 `forwarder.tools.call` span unification** with gRPC server span.\n\nThese four items can land in parallel with the early P0–P3 semantic phases (different files, different reviewers) — schedule before semantic stack is feature-complete to avoid release risk concentration.\n\n### Research Flags\n\n**Phases needing deeper research during planning:**\n- **P1:** stable symbol ID edge cases per language (overloads, generics, anonymous closures, decorators).\n- **P2:** fsnotify atomic-rename behavior across Vim/JetBrains/VS Code; ENOSPC fallback ergonomics.\n- **P9:** safety-receipt schema is novel; design must hold up against agent context-compaction and `enforce`-mode false-positive rate.\n- **P10:** task-class DoD definitions and scoring rubric; provider data-retention TOS re-verification.\n- **P11:** comment-format grammar subset selection.\n\n**Standard patterns (skip deep research):** P0, P0.5, P3, P4 (MVP), P6, P7, P8.\n\n## Confidence Assessment\n\n| Area | Confidence | Notes |\n|------|------------|-------|\n| Stack | HIGH | DuckDB binding, fsnotify gaps, gonum-vs-hand-rolled, eval libs verified at file/issue level. MEDIUM only on `tiktoken-go/tokenizer` and `bluekeyes/go-gitdiff` (READMEs not deep-verified). |\n| Features | HIGH on adjacent products + eval conventions; MEDIUM on Cursor/Cody/Augment freshness behavior (closed source); LOW on guardrail-receipt schema (no prior art). |\n| Architecture | HIGH | Verified against `internal/daemon/daemon.go`, SPEC §5/6/24/36/39, v1.9 callback patterns. MEDIUM on test-harness `:memory:` ergonomics and forwarder gRPC propagation for eval subprocess. |\n| Pitfalls | HIGH for items grounded in shipped Helix bugs (BUG-01..04, Phase 56) and upstream issues (fsnotify #17/#80/#214/#254/#372, DuckDB #77/#4899); MEDIUM on cross-product war stories. |\n\n**Overall confidence:** HIGH. Decision-defining choices (Layer 1.5 placement, DuckDB CGO posture + auto-disable, postEditHook callback, lookup-callback inversion for repomap, guardrail middleware position 14b.5, strangler-fig phasegraph for new graphs only, out-of-process eval harness) all anchored in v1.9 source and SPEC contracts. Unknowns isolated to novel surfaces (guardrail receipts, eval scoring rubric) and dynamic-language type-resolution edges.\n\n### Gaps to Address During Planning\n\n- **DuckDB driver canonical path.** STACK.md notes `marcboeker/go-duckdb` → `duckdb/duckdb-go` donation; ARCHITECTURE.md still references the legacy path. Lock import path during P0.\n- **Test harness DuckDB strategy.** `test/harness/Runner` constructs daemons in-process; with semantic enabled every test creates a `.duckdb` file unless `:memory:` is used. Confirm at P0.\n- **Forwarder gRPC propagation of semantic config** for eval subprocess (likely `HELIX_CONFIG_PATH`); confirm at P10.\n- **Bootstrap step renumbering convention.** Decimal additions (2.5 / 12e–12i / 14b.5) cross 16+ steps; reviewer ergonomics may justify a one-time renumber.\n- **Receipt TTL × `graph_version` invalidation interplay.** Long-running guardrail-warned operations need P9 prototype.\n- **Eval task corpus.** Synthetic-only is the threat-model rule but has lower discriminating power; OSS Helix repo + permissively-licensed SWE-bench Verified subset are options. Lock at P10.\n- **Provider data-retention shifts.** Re-verify Anthropic/OpenAI/DeepSeek TOS at P10 implementation.\n\n## Sources\n\n**Primary (HIGH):** `SPEC-DRAFT.md` (40 sections), `.planning/PROJECT.md`, `internal/daemon/daemon.go` (lines 199-203, 241-255, 296-336, 349-380, 481-525, 765-825), `internal/mcp/lazy_init.go:106-108`, `CLAUDE.md`. [duckdb/duckdb-go](https://github.com/duckdb/duckdb-go), [DuckDB Concurrency](https://duckdb.org/docs/current/connect/concurrency), [fsnotify](https://github.com/fsnotify/fsnotify) issues #17/#80/#214/#254/#372, [gonum graph/network](https://pkg.go.dev/gonum.org/v1/gonum/graph/network), [SCIP](https://sourcegraph.com/blog/announcing-scip), [Glean](https://engineering.fb.com/2024/12/19/developer-tools/glean-open-source-code-indexing/), [Stack graphs](https://github.blog/open-source/introducing-stack-graphs/), [Aider repomap](https://aider.chat/docs/repomap.html), [SWE-bench](https://www.swebench.com/).\n\n**Secondary (MEDIUM):** [tiktoken-go/tokenizer](https://github.com/tiktoken-go/tokenizer), [bluekeyes/go-gitdiff](https://github.com/bluekeyes/go-gitdiff) (READMEs only), [Cursor indexing](https://towardsdatascience.com/how-cursor-actually-indexes-your-codebase/), [Copilot guardrails](https://docs.github.com/en/copilot/tutorials/cloud-agent/build-guardrails), pyright/sorbet/Phan/Psalm tier conventions.\n\n**Tertiary (LOW — needs validation):** Sourcegraph zoekt / gopls overlay-correctness post-mortems; Anthropic/OpenAI/DeepSeek TOS (shifting); guardrail-receipt prior art (none found, novel design needs P9 prototype).\n"}}}} |