Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 5 additions & 1 deletion dsl/extdeps/github/github.dag
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,15 @@

module extdeps.github.github

import std.serialization { VariantEncoding, StringVariant, VariantNaming, AsAuthored }
import std.types { Timestamp }

// Wire contract: GitHub API returns enum values as plain strings ("User", "Bot").
data wire_contract: VariantEncoding = StringVariant { naming: AsAuthored }

// -- Users ---------------------------------------------------------

type UserType = HumanUser | Bot | Organization
type UserType = User | Bot | Organization

type GitHubUser {
login: String
Expand Down
4 changes: 2 additions & 2 deletions dsl/extdeps/github/pulls.dag
Original file line number Diff line number Diff line change
Expand Up @@ -97,7 +97,7 @@ service github.Pulls {
body: "Implements widget rendering",
state: "open",
html_url: "https://github.com/mock-owner/mock-repo/pull/42",
user: { login: "mock-user", id: 1, type: { _variant: "HumanUser" } },
user: { login: "mock-user", id: 1, type: "User" },
head: { ref: "feature-widgets", sha: "abc1234def5678", label: "mock-user:feature-widgets" },
base: { ref: "main", sha: "000aaa111bbb", label: "mock-owner:main" },
created_at: "2026-03-01T10:00:00Z",
Expand Down Expand Up @@ -137,7 +137,7 @@ service github.Pulls {
body: "Implements widget rendering",
state: "open",
html_url: "https://github.com/mock-owner/mock-repo/pull/42",
user: { login: "mock-user", id: 1, type: { _variant: "HumanUser" } },
user: { login: "mock-user", id: 1, type: "User" },
head: { ref: "feature-widgets", sha: "abc1234def5678", label: "mock-user:feature-widgets" },
base: { ref: "main", sha: "000aaa111bbb", label: "mock-owner:main" },
created_at: "2026-03-01T10:00:00Z",
Expand Down
9 changes: 8 additions & 1 deletion dsl/extdeps/llm/anthropic.dag
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,13 @@ module extdeps.llm.anthropic

import extdeps.llm.llm { Role, StopReason, TokenUsage }
import std.errors { AnthropicErrorShape }
import std.credentials { CredentialSource, EnvVar }
import std.serialization { VariantEncoding, StringVariant, VariantNaming, SnakeCase }
import std.types { HttpMethod }

// Wire contract: Anthropic API returns enum values as snake_case strings.
data wire_contract: VariantEncoding = StringVariant { naming: SnakeCase }

// -- Models --------------------------------------------------------

type AnthropicModel
Expand Down Expand Up @@ -88,6 +93,7 @@ service llm.Anthropic {
endpoint: "https://api.anthropic.com"
auth: Header("x-api-key")
auth_input: api_key
auth_source: EnvVar { name: "ANTHROPIC_API_KEY" }

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: auth_source adds env-based credential authority while Messages still exposes api_key as an operation input, violating Single-authority metadata and Explicit boundary contracts.

// Anthropic API rate limits: varies by tier (1000-4000 RPM), use conservative default
rate_limit: { requests: 1000, per: minute, scope: messages }
// Retry on overload and server errors with exponential backoff
Expand All @@ -113,7 +119,8 @@ service llm.Anthropic {
transport rest {
method: POST,
path: "/v1/messages",
body: { model: model, messages: messages, max_tokens: max_tokens, temperature: temperature, system: system }
body: { model: model, messages: messages, max_tokens: max_tokens, temperature: temperature, system: system },
headers: { "anthropic-version": current_api_version.version }
}
response {
200 => Json
Expand Down
9 changes: 8 additions & 1 deletion dsl/extdeps/llm/llm.dag
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@

module extdeps.llm.llm

import std.serialization { VariantEncoding, StringVariant, VariantNaming, SnakeCase }

// Wire contract: LLM APIs return enum values as snake_case strings ("user", "end_turn").
data wire_contract: VariantEncoding = StringVariant { naming: SnakeCase }

// -- Message protocol ----------------------------------------------

type Role = LlmSystem | LlmUser | LlmAssistant | LlmTool
// Variant names match API wire strings under SnakeCase:
// System → "system", User → "user", Assistant → "assistant", Tool → "tool"
type Role = System | User | Assistant | Tool

type LlmMessage {
role: Role
Expand Down
4 changes: 4 additions & 0 deletions dsl/extdeps/llm/openai.dag
Original file line number Diff line number Diff line change
Expand Up @@ -10,8 +10,12 @@ module extdeps.llm.openai

import extdeps.llm.llm { Role, StopReason, TokenUsage }
import std.errors { OpenAiErrorShape }
import std.serialization { VariantEncoding, StringVariant, VariantNaming, SnakeCase }
import std.types { HttpMethod, AuthScheme }

// Wire contract: OpenAI API returns enum values as snake_case strings.
data wire_contract: VariantEncoding = StringVariant { naming: SnakeCase }

// -- Models --------------------------------------------------------

type OpenAiModel
Expand Down
78 changes: 47 additions & 31 deletions dsl/gunbc/tools/review.dag
Original file line number Diff line number Diff line change
Expand Up @@ -14,30 +14,44 @@ module gunbc.tools.review

import extdeps.cron
import extdeps.github.pulls
import extdeps.llm.cli
import extdeps.llm.anthropic
import extdeps.shell

// -- Configuration as data --------------------------------------------

data default_backend: String = "codex"
data default_model: String = "gpt-5.4"
data default_model: String = "claude-sonnet-4-6-20250929"

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Invariant violation: default_model re-literals Anthropic's model id instead of reusing the provider authority already declared in anthropic.dag, violating Single-authority metadata and MODELING.md M7.

data cron_schedule: String = "*/10 * * * *"
// BRIDGE: messages should be [{ role: "user", content: "..." }] (Json array),
// but emit_simple_expr cannot render list/record literals in transport bodies.
// Passed as a JSON-encoded string; the emitter coerces via serde_json::from_str.
// Dissolves when emit_simple_expr handles ExprListLit/ExprRecordLit.
data review_messages: String = "[{\"role\": \"user\", \"content\": \"Review the PR described in the system prompt.\"}]"

// -- Pure functions ---------------------------------------------------

fn backend_command(backend: String, model: String, prompt_file: String, cwd: String) -> String {
match backend {
"codex" => "codex exec -m {model} -C {cwd} - < {prompt_file}"
"claude" => "claude -p --output-format text --permission-mode plan --model {model} --effort max < {prompt_file}"
"gemini" => "gemini -p --output-format text --approval-mode plan < {prompt_file}"
_ => "echo unknown backend && exit 1"
}
}

fn has_review_at_sha(reviews: List<PullReview>, sha: String) -> Bool {
any(reviews, r => r.commit_id == sha && contains(r.body, "Review"))
}

fn build_system_prompt(
invariants: String,
algebra_ref: String,
extdeps_ref: String
) -> String {
let instructions = "Review on two axes:\n1. INVARIANT VIOLATIONS: check diff against INVARIANTS.md\n2. COMPOSITIONAL MODELING: for .dag changes, evaluate accuracy, harmony, layering\n\nOutput:\nVIOLATION|file|line|description\nMODEL|file|GOOD|description\nMODEL|file|IMPROVE|description\nSUMMARY|one sentence\n\nNothing else."
"{instructions}\n\n--- INVARIANTS.md ---\n{invariants}\n\n--- std/algebra.dag ---\n{algebra_ref}\n\n--- extdeps.md ---\n{extdeps_ref}"
}

fn build_user_message(
pr_title: String,
pr_number: Int,
branch: String,
base_ref: String,
diff: String
) -> String {
"PR #{pr_number}: {pr_title}\nBranch: {branch} -> {base_ref}\n\n--- Diff ---\n{diff}"
}

fn build_review_prompt(
pr_title: String,
pr_number: Int,
Expand All @@ -48,9 +62,9 @@ fn build_review_prompt(
algebra_ref: String,
extdeps_ref: String
) -> String {
let instructions = "Review on two axes:\n1. INVARIANT VIOLATIONS: check diff against INVARIANTS.md\n2. COMPOSITIONAL MODELING: for .dag changes, evaluate accuracy, harmony, layering\n\nOutput:\nVIOLATION|file|line|description\nMODEL|file|GOOD|description\nMODEL|file|IMPROVE|description\nSUMMARY|one sentence\n\nNothing else."
let header = "PR #{pr_number}: {pr_title}\nBranch: {branch} -> {base_ref}"
"{instructions}\n\n{header}\n\n--- INVARIANTS.md ---\n{invariants}\n\n--- std/algebra.dag ---\n{algebra_ref}\n\n--- extdeps.md ---\n{extdeps_ref}\n\n--- Diff ---\n{diff}"
let system = build_system_prompt(invariants: invariants, algebra_ref: algebra_ref, extdeps_ref: extdeps_ref)
let user_msg = build_user_message(pr_title: pr_title, pr_number: pr_number, branch: branch, base_ref: base_ref, diff: diff)
"{system}\n\n{user_msg}"
}

// -- Review a single PR -----------------------------------------------
Expand All @@ -61,7 +75,6 @@ func review_pr(
owner: String,
repo: String,
pr_number: Int,
backend: String = default_backend,
model: String = default_model
) -> { reviewed: Bool, comment_url: String } {

Expand Down Expand Up @@ -99,21 +112,26 @@ func review_pr(
script: "cd ~/gunbc && git show origin/{pr.pr.head.ref}:dsl/extdeps/extdeps.md 2>/dev/null || echo '(not found)'"
)

// Step 4: Build prompt and invoke LLM
prompt = build_review_prompt(
// Step 4: Build context and invoke LLM via Anthropic REST API
system_prompt = build_system_prompt(
invariants: invariants.stdout,
algebra_ref: algebra_ref.stdout,
extdeps_ref: extdeps_ref.stdout
)
user_msg = build_user_message(
pr_title: pr.pr.title,
pr_number: pr_number,
branch: pr.pr.head.ref,
base_ref: pr.pr.base.ref,
diff: diff.diff,
invariants: invariants.stdout,
algebra_ref: algebra_ref.stdout,
extdeps_ref: extdeps_ref.stdout
diff: diff.diff
)
prompt_file = "/Users/briansrls/.local/share/gunbc-review/prompt.md"
command = backend_command(backend: backend, model: model, prompt_file: prompt_file, cwd: "/Users/briansrls/gunbc")
llm_result = shell.Exec.Run(
script: "mkdir -p /Users/briansrls/.local/share/gunbc-review && printf '%s' '{prompt}' > {prompt_file} && {command}"
llm_response = llm.Anthropic.Messages(

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Respect selected backend when invoking the review model

review_pr still accepts a backend parameter (and review_cycle passes it through), but this code unconditionally calls llm.Anthropic.Messages. That means callers who set backend to another value now get silently routed to Anthropic and can fail due to missing ANTHROPIC_API_KEY, which is a behavioral regression from the prior backend-switching flow.

Useful? React with 👍 / 👎.

api_key: "",
model: model,
messages: review_messages,
max_tokens: 8192,
temperature: none,
system: "{system_prompt}\n\n{user_msg}"
)

// Step 5: Post review via GitHub API (idempotent: guarded by step 2)
Expand All @@ -123,7 +141,7 @@ func review_pr(
repo: repo,
pull_number: pr_number,
commit_id: pr.pr.head.sha,
body: llm_result.stdout,
body: llm_response.content,
comments: "[]"
)

Expand All @@ -138,8 +156,7 @@ func review_pr(

func review_cycle(
owner: String = "gunb-ai",
repo: String = "gunbc",
backend: String = default_backend
repo: String = "gunbc"
) -> { reviewed_count: Int } {

// Upsert cron -- idempotent, converges on every run
Expand All @@ -163,8 +180,7 @@ func review_cycle(
review_pr(
owner: owner,
repo: repo,
pr_number: pr.number,
backend: backend
pr_number: pr.number
)
}

Expand Down
29 changes: 29 additions & 0 deletions dsl/std/serialization.dag
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
// std/serialization.dag -- Wire format contracts for external APIs.
//
// Types declare how their values appear on the wire (JSON responses).
// Modules declare a wire_contract that applies to all types within.
// The emitter reads the contract and generates target-language
// serialization attributes (e.g., serde for Rust).
//
// Without a wire_contract, enums use internal tagging ({"_variant": "Name"}).
// With StringVariant, enums serialize as plain strings ("Name" or "name").

module std.serialization

// How enum variant names map to wire strings.
// AsAuthored: variant name as-is (User → "User", Bot → "Bot").
// SnakeCase: variant name lowercased with underscores (EndTurn → "end_turn").
type VariantNaming = AsAuthored | SnakeCase

// How enum variants appear on the wire.
// StringVariant: plain strings — the common REST API pattern.
// TaggedVariant: internally tagged objects ({"_variant": "Name"}) — .dag default.
type VariantEncoding
= StringVariant { naming: VariantNaming }
| TaggedVariant

// Module-level wire contract. Declared as:
// data wire_contract: WireContract = { variant_encoding: StringVariant { naming: AsAuthored } }
type WireContract {
variant_encoding: VariantEncoding
}
Loading
Loading