• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

marscoin / martianrepublic / 23804883405

31 Mar 2026 03:14PM UTC coverage: 11.195% (+1.8%) from 9.347%
23804883405

push

github

Martian Congress
refactor: Tier 1 — security fixes, route standardization, code style, docs

S9:  File upload path traversal — assert realpath() within allowed dirs,
     sanitize citizen address in file paths
S11: Citizen registry queries capped with LIMIT 100
S12: External HTTP calls — replaced file_get_contents with Http::timeout()
     in CongressController, Wallet/ApiController, AppHelper
A8:  All routes converted to Laravel 9+ array notation
     [Controller::class, 'method']
D2:  Created DEVELOPMENT.md — complete setup guide for contributors
D4:  Laravel Pint code style enforced across 211 files

Tests updated: public route assertions corrected after route syntax
standardization exposed that old string-notation routes never resolved
properly in tests.

127 tests, 251 assertions, 0 PHPStan errors.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

302 of 3262 new or added lines in 58 files covered. (9.26%)

139 existing lines in 24 files now uncovered.

620 of 5538 relevant lines covered (11.2%)

1.54 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

29.27
/app/Http/Controllers/AiHelperController.php
1
<?php
2

3
namespace App\Http\Controllers;
4

5
use Illuminate\Http\Request;
6
use Illuminate\Support\Facades\Http;
7
use Symfony\Component\HttpFoundation\StreamedResponse;
8

9
class AiHelperController extends Controller
10
{
11
    private function getSystemPrompt(): string
12
    {
13
        return <<<'PROMPT'
14
You are Olympus, the AI guide of the Martian Republic — a blockchain-based governance platform for Mars built on the Marscoin network.
15

16
Your role: Help citizens and newcomers understand how the Republic works. Be warm, concise, and jargon-free. You embody civic pride and the pioneer spirit of Mars settlement.
17

18
You have access to a search tool to look up Academy articles and site content. Use it when you need specific details about governance, voting, Mars time, or any topic covered in the Academy. Don't guess — search first if unsure.
19

20
Key knowledge:
21
- Direct democracy on the Marscoin blockchain
22
- 4 governance tiers: Signal, Operational, Legislative, Constitutional
23
- CoinShuffle for anonymous voting (cryptographic ballot mixing)
24
- Citizenship requires registration, 2FA, and an HD wallet ("The Forge")
25
- Mars time: 1 sol = 24h 39m 35s, Darian calendar (24 months, 668.6 sols/year)
26
- MARS cryptocurrency: proof-of-work, merge-mined with Litecoin
27

28
Academy articles:
29
- /academy/governance-and-voting — 4-tier governance system
30
- /academy/coinshuffle-secret-ballots — anonymous voting protocol
31
- /academy/mars-timekeeping — sols, MTC, Darian calendar
32
- /academy/the-public-good — political philosophy
33

34
Rules:
35
- Keep answers under 150 words unless asked for detail
36
- Never make up information — use the search tool if unsure
37
- Link to Academy articles when relevant (use markdown links)
38
- No price speculation or investment advice
39
- Be encouraging — every citizen matters
40
PROMPT;
41
    }
42

43
    private function getTools(): array
1✔
44
    {
45
        return [
1✔
46
            [
1✔
47
                'type' => 'function',
1✔
48
                'function' => [
1✔
49
                    'name' => 'search_content',
1✔
50
                    'description' => 'Search the Martian Republic Academy articles and site content for information. Use this to find accurate details about governance, voting, Mars time, citizenship, proposals, or any topic.',
1✔
51
                    'parameters' => [
1✔
52
                        'type' => 'object',
1✔
53
                        'properties' => [
1✔
54
                            'query' => [
1✔
55
                                'type' => 'string',
1✔
56
                                'description' => 'Search query — keywords or a question to search for in Academy articles',
1✔
57
                            ],
1✔
58
                        ],
1✔
59
                        'required' => ['query'],
1✔
60
                    ],
1✔
61
                ],
1✔
62
            ],
1✔
63
        ];
1✔
64
    }
65

66
    private function executeSearch(string $query): string
×
67
    {
68
        $searchPaths = [
×
69
            resource_path('views/academy'),
×
70
            resource_path('views/academy/articles'),
×
71
        ];
×
72

73
        $keywords = array_filter(
×
74
            explode(' ', strtolower(preg_replace('/[^a-zA-Z0-9\s]/', '', $query))),
×
NEW
75
            fn ($w) => strlen($w) > 2
×
76
        );
×
77

NEW
78
        if (empty($keywords)) {
×
NEW
79
            return 'No results found.';
×
80
        }
81

82
        $results = [];
×
83

84
        foreach ($searchPaths as $path) {
×
NEW
85
            $files = glob($path.'/*.blade.php') ?: [];
×
86
            foreach ($files as $file) {
×
87
                $content = file_get_contents($file);
×
88
                $text = strip_tags($content);
×
89
                $text = preg_replace('/\s+/', ' ', $text);
×
90
                $lower = strtolower($text);
×
91

92
                $score = 0;
×
93
                foreach ($keywords as $kw) {
×
94
                    $score += substr_count($lower, $kw);
×
95
                }
96

97
                if ($score > 0) {
×
98
                    $slug = basename($file, '.blade.php');
×
99
                    // Get multiple excerpts around matches
100
                    $excerpts = [];
×
101
                    foreach ($keywords as $kw) {
×
102
                        $offset = 0;
×
103
                        while (($pos = strpos($lower, $kw, $offset)) !== false && count($excerpts) < 3) {
×
104
                            $start = max(0, $pos - 80);
×
NEW
105
                            $excerpts[] = '...'.trim(substr($text, $start, 250)).'...';
×
106
                            $offset = $pos + strlen($kw) + 100;
×
107
                        }
108
                    }
109
                    $results[] = [
×
110
                        'score' => $score,
×
111
                        'slug' => $slug,
×
NEW
112
                        'url' => '/academy/'.$slug,
×
113
                        'excerpts' => array_unique(array_slice($excerpts, 0, 3)),
×
114
                    ];
×
115
                }
116
            }
117
        }
118

NEW
119
        usort($results, fn ($a, $b) => $b['score'] <=> $a['score']);
×
120
        $top = array_slice($results, 0, 3);
×
121

NEW
122
        if (empty($top)) {
×
NEW
123
            return 'No matching Academy articles found for: '.$query;
×
124
        }
125

126
        $output = "Search results for \"{$query}\":\n\n";
×
127
        foreach ($top as $r) {
×
128
            $output .= "--- {$r['url']} (relevance: {$r['score']}) ---\n";
×
129
            foreach ($r['excerpts'] as $ex) {
×
NEW
130
                $output .= $ex."\n";
×
131
            }
132
            $output .= "\n";
×
133
        }
134

UNCOV
135
        return $output;
×
136
    }
137

138
    public function chat(Request $request)
3✔
139
    {
140
        $request->validate([
3✔
141
            'messages' => 'required|array|min:1',
3✔
142
            'messages.*.role' => 'required|in:user,assistant',
3✔
143
            'messages.*.content' => 'required|string|max:2000',
3✔
144
        ]);
3✔
145

146
        $userMessages = array_slice($request->input('messages'), -10);
1✔
147
        $apiKey = config('services.openrouter.api_key');
1✔
148
        $model = config('services.openrouter.model', 'qwen/qwen3-235b-a22b-2507');
1✔
149

150
        $messages = array_merge(
1✔
151
            [['role' => 'system', 'content' => $this->getSystemPrompt()]],
1✔
152
            $userMessages
1✔
153
        );
1✔
154

155
        // Step 1: Non-streaming call to check for tool use
156
        $firstResponse = Http::withHeaders([
1✔
157
            'Authorization' => 'Bearer '.$apiKey,
1✔
158
            'HTTP-Referer' => 'https://martianrepublic.org',
1✔
159
            'X-Title' => 'Martian Republic',
1✔
160
        ])->timeout(30)->post('https://openrouter.ai/api/v1/chat/completions', [
1✔
161
            'model' => $model,
1✔
162
            'messages' => $messages,
1✔
163
            'tools' => $this->getTools(),
1✔
164
            'max_tokens' => 500,
1✔
165
            'temperature' => 0.7,
1✔
166
        ]);
1✔
167

168
        $firstData = $firstResponse->json();
1✔
169
        $choice = $firstData['choices'][0] ?? null;
1✔
170

171
        if (! $choice) {
1✔
172
            return response()->json(['error' => 'No response from AI'], 502);
1✔
173
        }
174

175
        // Step 2: If tool call, execute and do a second call with results
176
        if (($choice['finish_reason'] ?? '') === 'tool_calls' ||
×
NEW
177
            ! empty($choice['message']['tool_calls'])) {
×
178

179
            $toolCalls = $choice['message']['tool_calls'] ?? [];
×
180
            $assistantMsg = $choice['message'];
×
181
            $messages[] = $assistantMsg;
×
182

183
            foreach ($toolCalls as $tc) {
×
184
                $args = json_decode($tc['function']['arguments'] ?? '{}', true);
×
185
                $searchQuery = $args['query'] ?? '';
×
186
                $result = $this->executeSearch($searchQuery);
×
187

188
                $messages[] = [
×
189
                    'role' => 'tool',
×
190
                    'tool_call_id' => $tc['id'],
×
191
                    'content' => $result,
×
192
                ];
×
193
            }
194

195
            // Step 3: Stream the final response with tool results
196
            return $this->streamResponse($messages, $apiKey, $model);
×
197
        }
198

199
        // No tool call — stream directly from the first response content
200
        $content = $choice['message']['content'] ?? '';
×
201

202
        return new StreamedResponse(function () use ($content) {
×
NEW
203
            echo 'data: '.json_encode(['content' => $content])."\n\n";
×
204
            echo "data: [DONE]\n\n";
×
205
            ob_flush();
×
206
            flush();
×
207
        }, 200, [
×
208
            'Content-Type' => 'text/event-stream',
×
209
            'Cache-Control' => 'no-cache',
×
210
            'Connection' => 'keep-alive',
×
211
            'X-Accel-Buffering' => 'no',
×
212
        ]);
×
213
    }
214

215
    private function streamResponse(array $messages, string $apiKey, string $model): StreamedResponse
×
216
    {
217
        return new StreamedResponse(function () use ($messages, $apiKey, $model) {
×
218
            $response = Http::withHeaders([
×
NEW
219
                'Authorization' => 'Bearer '.$apiKey,
×
220
                'HTTP-Referer' => 'https://martianrepublic.org',
×
221
                'X-Title' => 'Martian Republic',
×
222
            ])->withOptions([
×
223
                'stream' => true,
×
224
            ])->post('https://openrouter.ai/api/v1/chat/completions', [
×
225
                'model' => $model,
×
226
                'messages' => $messages,
×
227
                'stream' => true,
×
228
                'max_tokens' => 500,
×
229
                'temperature' => 0.7,
×
230
            ]);
×
231

232
            $body = $response->getBody();
×
NEW
233
            while (! $body->eof()) {
×
234
                $line = '';
×
NEW
235
                while (! $body->eof()) {
×
236
                    $char = $body->read(1);
×
NEW
237
                    if ($char === "\n") {
×
NEW
238
                        break;
×
239
                    }
UNCOV
240
                    $line .= $char;
×
241
                }
242
                $line = trim($line);
×
243
                if (str_starts_with($line, 'data: ')) {
×
244
                    $data = substr($line, 6);
×
245
                    if ($data === '[DONE]') {
×
246
                        echo "data: [DONE]\n\n";
×
247
                        break;
×
248
                    }
249
                    $json = json_decode($data, true);
×
250
                    if ($json && isset($json['choices'][0]['delta']['content'])) {
×
251
                        $token = $json['choices'][0]['delta']['content'];
×
NEW
252
                        echo 'data: '.json_encode(['content' => $token])."\n\n";
×
253
                        ob_flush();
×
254
                        flush();
×
255
                    }
256
                }
257
            }
258
        }, 200, [
×
259
            'Content-Type' => 'text/event-stream',
×
260
            'Cache-Control' => 'no-cache',
×
261
            'Connection' => 'keep-alive',
×
262
            'X-Accel-Buffering' => 'no',
×
263
        ]);
×
264
    }
265
}
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE TRIAL · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2026 Coveralls, Inc