@a-company/sentinel 0.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,854 @@
1
+ import {
2
+ SentinelStorage
3
+ } from "../chunk-KPMG4XED.js";
4
+
5
+ // src/server/index.ts
6
+ import express from "express";
7
+ import * as path3 from "path";
8
+ import * as fs2 from "fs";
9
+ import { fileURLToPath } from "url";
10
+ import chalk3 from "chalk";
11
+
12
+ // src/server/routes/symbols.ts
13
+ import { Router } from "express";
14
+ import chalk2 from "chalk";
15
+
16
+ // src/server/loaders/symbols.ts
17
+ import * as fs from "fs";
18
+ import * as path from "path";
19
+ import chalk from "chalk";
20
+ var LOG_LEVEL = process.env.SENTINEL_LOG_LEVEL || process.env.LOG_LEVEL || "info";
21
+ var LOG_LEVELS = { debug: 0, info: 1, warn: 2, error: 3 };
22
+ function shouldLog(level) {
23
+ return LOG_LEVELS[level] >= LOG_LEVELS[LOG_LEVEL];
24
+ }
25
+ function formatData(data) {
26
+ if (!data) return "";
27
+ const entries = Object.entries(data).map(([k, v]) => `${k}=${typeof v === "string" ? v : JSON.stringify(v)}`).join(" ");
28
+ return chalk.gray(` ${entries}`);
29
+ }
30
+ var log = {
31
+ component(name) {
32
+ const symbol = chalk.magenta(`#${name}`);
33
+ return {
34
+ debug: (msg, data) => {
35
+ if (shouldLog("debug")) console.log(`${chalk.gray("\u25CB")} ${symbol} ${msg}${formatData(data)}`);
36
+ },
37
+ info: (msg, data) => {
38
+ if (shouldLog("info")) console.log(`${chalk.blue("\u2139")} ${symbol} ${msg}${formatData(data)}`);
39
+ },
40
+ warn: (msg, data) => {
41
+ if (shouldLog("warn")) console.log(`${chalk.yellow("\u26A0")} ${symbol} ${msg}${formatData(data)}`);
42
+ },
43
+ error: (msg, data) => {
44
+ if (shouldLog("error")) console.error(`${chalk.red("\u2716")} ${symbol} ${msg}${formatData(data)}`);
45
+ }
46
+ };
47
+ },
48
+ flow(name) {
49
+ const symbol = chalk.yellow(`$${name}`);
50
+ return {
51
+ debug: (msg, data) => {
52
+ if (shouldLog("debug")) console.log(`${chalk.gray("\u25CB")} ${symbol} ${msg}${formatData(data)}`);
53
+ },
54
+ info: (msg, data) => {
55
+ if (shouldLog("info")) console.log(`${chalk.blue("\u2139")} ${symbol} ${msg}${formatData(data)}`);
56
+ },
57
+ warn: (msg, data) => {
58
+ if (shouldLog("warn")) console.log(`${chalk.yellow("\u26A0")} ${symbol} ${msg}${formatData(data)}`);
59
+ },
60
+ error: (msg, data) => {
61
+ if (shouldLog("error")) console.error(`${chalk.red("\u2716")} ${symbol} ${msg}${formatData(data)}`);
62
+ }
63
+ };
64
+ }
65
+ };
66
+ var SYMBOL_BLOCKLIST = /* @__PURE__ */ new Set([
67
+ "$lib",
68
+ "$env",
69
+ "$app",
70
+ "$service-worker",
71
+ "$virtual",
72
+ "$schema",
73
+ "$ref",
74
+ "$id",
75
+ "$type"
76
+ ]);
77
+ async function loadParadigmConfig(projectDir) {
78
+ const configPath = path.join(projectDir, ".paradigm", "config.yaml");
79
+ if (!fs.existsSync(configPath)) {
80
+ const packagePath = path.join(projectDir, "package.json");
81
+ if (fs.existsSync(packagePath)) {
82
+ try {
83
+ const pkg = JSON.parse(fs.readFileSync(packagePath, "utf-8"));
84
+ return { name: pkg.name };
85
+ } catch {
86
+ }
87
+ }
88
+ return {};
89
+ }
90
+ try {
91
+ const content = fs.readFileSync(configPath, "utf-8");
92
+ const config = {};
93
+ const nameMatch = content.match(/^name:\s*(.+)$/m);
94
+ if (nameMatch) config.name = nameMatch[1].trim().replace(/^["']|["']$/g, "");
95
+ const disciplineMatch = content.match(/^discipline:\s*(.+)$/m);
96
+ if (disciplineMatch) config.discipline = disciplineMatch[1].trim();
97
+ const versionMatch = content.match(/^version:\s*(.+)$/m);
98
+ if (versionMatch) config.version = versionMatch[1].trim();
99
+ return config;
100
+ } catch (error) {
101
+ log.component("config-loader").error("Failed to load Paradigm config", { error: String(error) });
102
+ return {};
103
+ }
104
+ }
105
+ async function loadWithPremiseCore(projectDir) {
106
+ try {
107
+ const { aggregateFromDirectory } = await import("../dist-2F7NO4H4.js");
108
+ log.flow("load-symbols").info("Using premise-core aggregator", { path: projectDir });
109
+ const result = await aggregateFromDirectory(projectDir);
110
+ const counts = {};
111
+ for (const sym of result.symbols) {
112
+ counts[sym.type] = (counts[sym.type] || 0) + 1;
113
+ }
114
+ log.flow("load-symbols").info("Aggregation complete", {
115
+ total: result.symbols.length,
116
+ ...counts,
117
+ purposeFiles: result.purposeFiles.length,
118
+ portalFiles: result.portalFiles.length
119
+ });
120
+ if (result.errors.length > 0) {
121
+ for (const err of result.errors) {
122
+ log.component("aggregator").warn("Aggregation error", {
123
+ source: err.source,
124
+ file: err.filePath,
125
+ message: err.message
126
+ });
127
+ }
128
+ }
129
+ for (const file of result.purposeFiles) {
130
+ log.component("purpose-loader").info("Loaded .purpose file", { file: path.relative(projectDir, file) });
131
+ }
132
+ for (const file of result.portalFiles) {
133
+ log.component("gate-loader").info("Loaded portal.yaml", { file: path.relative(projectDir, file) });
134
+ }
135
+ return result.symbols;
136
+ } catch (error) {
137
+ log.component("premise-core").warn("premise-core not available, using fallback scanner", {
138
+ error: error instanceof Error ? error.message : String(error)
139
+ });
140
+ return null;
141
+ }
142
+ }
143
+ async function loadSymbolIndex(projectDir) {
144
+ log.flow("load-symbols").info("Loading symbols", { projectDir });
145
+ const indexPath = path.join(projectDir, ".paradigm", "index.json");
146
+ if (fs.existsSync(indexPath)) {
147
+ try {
148
+ log.component("index-loader").info("Found cached index", { path: indexPath });
149
+ const content = fs.readFileSync(indexPath, "utf-8");
150
+ const index = JSON.parse(content);
151
+ const entries = Array.isArray(index.entries) ? index.entries : Array.isArray(index) ? index : null;
152
+ if (entries) {
153
+ log.flow("load-symbols").info("Loaded from cached index", { count: entries.length });
154
+ return entries;
155
+ }
156
+ } catch (error) {
157
+ log.component("index-loader").error("Failed to load cached index", { error: String(error) });
158
+ }
159
+ }
160
+ const premiseResult = await loadWithPremiseCore(projectDir);
161
+ if (premiseResult) {
162
+ return premiseResult;
163
+ }
164
+ log.flow("load-symbols").info("Using fallback scanner");
165
+ return scanPurposeFiles(projectDir);
166
+ }
167
+ async function scanPurposeFiles(projectDir) {
168
+ const symbols = [];
169
+ const seenIds = /* @__PURE__ */ new Set();
170
+ const scanDirs = ["src", "lib", "packages", "apps", "."];
171
+ for (const dir of scanDirs) {
172
+ const fullPath = path.join(projectDir, dir);
173
+ if (fs.existsSync(fullPath)) {
174
+ await scanDirectory(fullPath, symbols, seenIds, projectDir);
175
+ }
176
+ }
177
+ const portalPath = path.join(projectDir, "portal.yaml");
178
+ if (fs.existsSync(portalPath)) {
179
+ log.component("gate-loader").debug("Found portal.yaml", { path: "portal.yaml" });
180
+ try {
181
+ const content = fs.readFileSync(portalPath, "utf-8");
182
+ const gatesSection = content.match(/^gates:\s*\n((?: .+\n)*)/m);
183
+ if (gatesSection) {
184
+ const gateMatches = gatesSection[1].matchAll(/^ ([a-z][a-z0-9-]*):/gm);
185
+ for (const match of gateMatches) {
186
+ const gateName = match[1];
187
+ const id = `gate-${gateName}`;
188
+ if (!seenIds.has(id)) {
189
+ seenIds.add(id);
190
+ symbols.push({
191
+ id,
192
+ symbol: `^${gateName}`,
193
+ type: "gate",
194
+ source: "portal",
195
+ filePath: "portal.yaml",
196
+ data: {},
197
+ references: [],
198
+ referencedBy: []
199
+ });
200
+ log.component("gate-loader").debug("Extracted gate", { symbol: `^${gateName}` });
201
+ }
202
+ }
203
+ }
204
+ } catch (error) {
205
+ log.component("gate-loader").error("Failed to parse portal.yaml", { error: String(error) });
206
+ }
207
+ }
208
+ log.flow("load-symbols").info("Fallback scan complete", { count: symbols.length });
209
+ return symbols;
210
+ }
211
+ async function scanDirectory(dir, symbols, seenIds, projectDir) {
212
+ const skipDirs = ["node_modules", ".git", "dist", "build", ".paradigm", "coverage", ".next", ".svelte-kit"];
213
+ let entries;
214
+ try {
215
+ entries = fs.readdirSync(dir, { withFileTypes: true });
216
+ } catch {
217
+ return;
218
+ }
219
+ for (const entry of entries) {
220
+ const fullPath = path.join(dir, entry.name);
221
+ if (entry.isDirectory()) {
222
+ if (!skipDirs.includes(entry.name)) {
223
+ await scanDirectory(fullPath, symbols, seenIds, projectDir);
224
+ }
225
+ } else if (entry.name === ".purpose") {
226
+ const relativePath = path.relative(projectDir, fullPath);
227
+ log.component("purpose-loader").debug("Scanning .purpose file", { path: relativePath });
228
+ try {
229
+ const content = fs.readFileSync(fullPath, "utf-8");
230
+ const parsed = parsePurposeFile(content, fullPath, projectDir);
231
+ for (const symbol of parsed) {
232
+ if (!seenIds.has(symbol.id)) {
233
+ seenIds.add(symbol.id);
234
+ symbols.push(symbol);
235
+ log.component("purpose-loader").debug("Extracted symbol", {
236
+ symbol: symbol.symbol,
237
+ type: symbol.type,
238
+ file: relativePath
239
+ });
240
+ }
241
+ }
242
+ } catch (error) {
243
+ log.component("purpose-loader").error("Failed to parse .purpose file", {
244
+ path: relativePath,
245
+ error: String(error)
246
+ });
247
+ }
248
+ }
249
+ }
250
+ }
251
+ function parsePurposeFile(content, filePath, projectDir) {
252
+ const symbols = [];
253
+ const relativePath = path.relative(projectDir, filePath);
254
+ const componentMatches = content.matchAll(/(?:^|\s)#([a-z][a-z0-9-]*)/gm);
255
+ for (const match of componentMatches) {
256
+ const name = match[1];
257
+ symbols.push({
258
+ id: `component-${name}`,
259
+ symbol: `#${name}`,
260
+ type: "component",
261
+ source: "purpose",
262
+ filePath: relativePath,
263
+ data: {},
264
+ description: extractDescription(content, `#${name}`),
265
+ references: extractReferences(content),
266
+ referencedBy: [],
267
+ tags: extractTags(content)
268
+ });
269
+ }
270
+ const flowMatches = content.matchAll(/\$([a-z][a-z0-9-]*)/gm);
271
+ for (const match of flowMatches) {
272
+ const name = match[1];
273
+ const symbol = `$${name}`;
274
+ if (SYMBOL_BLOCKLIST.has(symbol)) {
275
+ log.component("purpose-loader").debug("Skipping blocklisted symbol", { symbol });
276
+ continue;
277
+ }
278
+ if (!symbols.find((s) => s.symbol === symbol)) {
279
+ symbols.push({
280
+ id: `flow-${name}`,
281
+ symbol,
282
+ type: "flow",
283
+ source: "purpose",
284
+ filePath: relativePath,
285
+ data: {},
286
+ references: [],
287
+ referencedBy: []
288
+ });
289
+ }
290
+ }
291
+ const signalMatches = content.matchAll(/!([a-z][a-z0-9-]*)/gm);
292
+ for (const match of signalMatches) {
293
+ const name = match[1];
294
+ if (!symbols.find((s) => s.symbol === `!${name}`)) {
295
+ symbols.push({
296
+ id: `signal-${name}`,
297
+ symbol: `!${name}`,
298
+ type: "signal",
299
+ source: "purpose",
300
+ filePath: relativePath,
301
+ data: {},
302
+ references: [],
303
+ referencedBy: []
304
+ });
305
+ }
306
+ }
307
+ const gateMatches = content.matchAll(/\^([a-z][a-z0-9-]*)/gm);
308
+ for (const match of gateMatches) {
309
+ const name = match[1];
310
+ if (!symbols.find((s) => s.symbol === `^${name}`)) {
311
+ symbols.push({
312
+ id: `gate-${name}`,
313
+ symbol: `^${name}`,
314
+ type: "gate",
315
+ source: "purpose",
316
+ filePath: relativePath,
317
+ data: {},
318
+ references: [],
319
+ referencedBy: []
320
+ });
321
+ }
322
+ }
323
+ const aspectMatches = content.matchAll(/~([a-z][a-z0-9-]*)/gm);
324
+ for (const match of aspectMatches) {
325
+ const name = match[1];
326
+ if (!symbols.find((s) => s.symbol === `~${name}`)) {
327
+ symbols.push({
328
+ id: `aspect-${name}`,
329
+ symbol: `~${name}`,
330
+ type: "aspect",
331
+ source: "purpose",
332
+ filePath: relativePath,
333
+ data: {},
334
+ references: [],
335
+ referencedBy: []
336
+ });
337
+ }
338
+ }
339
+ return symbols;
340
+ }
341
+ function extractDescription(content, symbol) {
342
+ const regex = new RegExp(`${symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&")}\\s*[-:]?\\s*(.+)`, "m");
343
+ const match = content.match(regex);
344
+ if (match && match[1]) {
345
+ return match[1].trim();
346
+ }
347
+ return void 0;
348
+ }
349
+ function extractReferences(content) {
350
+ const refs = /* @__PURE__ */ new Set();
351
+ const refMatches = content.matchAll(/[@#$!^~]([a-z][a-z0-9-]*)/g);
352
+ for (const match of refMatches) {
353
+ const symbol = match[0];
354
+ if (!SYMBOL_BLOCKLIST.has(symbol)) {
355
+ refs.add(symbol);
356
+ }
357
+ }
358
+ return Array.from(refs);
359
+ }
360
+ function extractTags(content) {
361
+ const tagMatch = content.match(/tags:\s*\[([^\]]+)\]/);
362
+ if (tagMatch) {
363
+ return tagMatch[1].split(",").map((t) => t.trim().replace(/^["']|["']$/g, ""));
364
+ }
365
+ return [];
366
+ }
367
+ async function getSymbolCount(projectDir) {
368
+ const symbols = await loadSymbolIndex(projectDir);
369
+ return symbols.length;
370
+ }
371
+ async function updateSymbol(projectDir, symbolId, updates) {
372
+ const symbols = await loadSymbolIndex(projectDir);
373
+ const symbol = symbols.find((s) => s.id === symbolId);
374
+ if (!symbol) {
375
+ return { success: false, error: "Symbol not found" };
376
+ }
377
+ const filePath = path.join(projectDir, symbol.filePath);
378
+ if (!fs.existsSync(filePath)) {
379
+ return { success: false, error: "Source file not found" };
380
+ }
381
+ try {
382
+ let content = fs.readFileSync(filePath, "utf-8");
383
+ let modified = false;
384
+ if (updates.description !== void 0) {
385
+ const symbolPattern = symbol.symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
386
+ const descRegex = new RegExp(`(${symbolPattern})\\s*[-:]?\\s*(.*)`, "m");
387
+ const match = content.match(descRegex);
388
+ if (match) {
389
+ const newLine = updates.description ? `${symbol.symbol}: ${updates.description}` : symbol.symbol;
390
+ content = content.replace(descRegex, newLine);
391
+ modified = true;
392
+ }
393
+ }
394
+ if (updates.tags !== void 0) {
395
+ const tagsStr = updates.tags.length > 0 ? `tags: [${updates.tags.map((t) => `"${t}"`).join(", ")}]` : "";
396
+ const tagsRegex = /^tags:\s*\[[^\]]*\]\s*$/m;
397
+ if (tagsRegex.test(content)) {
398
+ if (tagsStr) {
399
+ content = content.replace(tagsRegex, tagsStr);
400
+ } else {
401
+ content = content.replace(tagsRegex, "");
402
+ }
403
+ modified = true;
404
+ } else if (tagsStr) {
405
+ const symbolPattern = symbol.symbol.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
406
+ const symbolLineRegex = new RegExp(`(${symbolPattern}[^\\n]*\\n)`, "m");
407
+ const symbolMatch = content.match(symbolLineRegex);
408
+ if (symbolMatch) {
409
+ content = content.replace(symbolLineRegex, `$1${tagsStr}
410
+ `);
411
+ modified = true;
412
+ }
413
+ }
414
+ }
415
+ if (modified) {
416
+ content = content.replace(/\n{3,}/g, "\n\n");
417
+ fs.writeFileSync(filePath, content, "utf-8");
418
+ log.component("symbol-updater").info("Updated symbol", { symbol: symbol.symbol, file: symbol.filePath });
419
+ const indexPath = path.join(projectDir, ".paradigm", "index.json");
420
+ if (fs.existsSync(indexPath)) {
421
+ try {
422
+ const indexContent = fs.readFileSync(indexPath, "utf-8");
423
+ const index = JSON.parse(indexContent);
424
+ const entries = Array.isArray(index.entries) ? index.entries : index;
425
+ const entryIndex = entries.findIndex((e) => e.id === symbolId);
426
+ if (entryIndex >= 0) {
427
+ if (updates.description !== void 0) {
428
+ entries[entryIndex].description = updates.description;
429
+ }
430
+ if (updates.tags !== void 0) {
431
+ entries[entryIndex].tags = updates.tags;
432
+ }
433
+ if (Array.isArray(index.entries)) {
434
+ index.entries = entries;
435
+ fs.writeFileSync(indexPath, JSON.stringify(index, null, 2), "utf-8");
436
+ } else {
437
+ fs.writeFileSync(indexPath, JSON.stringify(entries, null, 2), "utf-8");
438
+ }
439
+ }
440
+ } catch {
441
+ }
442
+ }
443
+ return { success: true };
444
+ }
445
+ return { success: true };
446
+ } catch (error) {
447
+ log.component("symbol-updater").error("Failed to update symbol", { error: String(error) });
448
+ return { success: false, error: "Failed to write file" };
449
+ }
450
+ }
451
+
452
+ // src/server/routes/symbols.ts
453
+ var LOG_LEVEL2 = process.env.SENTINEL_LOG_LEVEL || process.env.LOG_LEVEL || "info";
454
+ var shouldLog2 = (level) => {
455
+ const levels = { debug: 0, info: 1, warn: 2, error: 3 };
456
+ return levels[level] >= levels[LOG_LEVEL2];
457
+ };
458
+ var log2 = {
459
+ gate(name) {
460
+ const symbol = chalk2.cyan(`^${name}`);
461
+ return {
462
+ info: (msg, data) => {
463
+ if (shouldLog2("info")) {
464
+ const dataStr = data ? chalk2.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
465
+ console.log(`${chalk2.blue("\u2139")} ${symbol} ${msg}${dataStr}`);
466
+ }
467
+ },
468
+ error: (msg, data) => {
469
+ if (shouldLog2("error")) {
470
+ const dataStr = data ? chalk2.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
471
+ console.error(`${chalk2.red("\u2716")} ${symbol} ${msg}${dataStr}`);
472
+ }
473
+ }
474
+ };
475
+ }
476
+ };
477
+ function createSymbolsRouter(projectDir) {
478
+ const router = Router();
479
+ router.get("/", async (_req, res) => {
480
+ try {
481
+ const symbols = await loadSymbolIndex(projectDir);
482
+ log2.gate("api-symbols").info("Symbols loaded", { count: symbols.length });
483
+ res.json({ symbols });
484
+ } catch (error) {
485
+ log2.gate("api-symbols").error("Failed to load symbols", { error: String(error) });
486
+ res.status(500).json({ error: "Failed to load symbols" });
487
+ }
488
+ });
489
+ router.put("/:id", async (req, res) => {
490
+ try {
491
+ const { id } = req.params;
492
+ const updates = req.body;
493
+ log2.gate("api-symbols").info("Update requested", { id, updates: JSON.stringify(updates) });
494
+ if (updates.tags && !Array.isArray(updates.tags)) {
495
+ res.status(400).json({ error: "Tags must be an array" });
496
+ return;
497
+ }
498
+ const result = await updateSymbol(projectDir, id, updates);
499
+ if (result.success) {
500
+ const symbols = await loadSymbolIndex(projectDir);
501
+ const updatedSymbol = symbols.find((s) => s.id === id);
502
+ log2.gate("api-symbols").info("Symbol updated", { id });
503
+ res.json({ success: true, symbol: updatedSymbol });
504
+ } else {
505
+ log2.gate("api-symbols").error("Update failed", { id, error: result.error });
506
+ res.status(400).json({ success: false, error: result.error });
507
+ }
508
+ } catch (error) {
509
+ log2.gate("api-symbols").error("Failed to update symbol", { error: String(error) });
510
+ res.status(500).json({ error: "Failed to update symbol" });
511
+ }
512
+ });
513
+ return router;
514
+ }
515
+
516
+ // src/server/routes/info.ts
517
+ import { Router as Router2 } from "express";
518
+ function createInfoRouter(projectDir) {
519
+ const router = Router2();
520
+ router.get("/", async (_req, res) => {
521
+ try {
522
+ const config = await loadParadigmConfig(projectDir);
523
+ const symbolCount = await getSymbolCount(projectDir);
524
+ res.json({
525
+ projectName: config.name || null,
526
+ discipline: config.discipline || null,
527
+ symbolCount,
528
+ projectDir
529
+ });
530
+ } catch (error) {
531
+ console.error("Failed to load project info:", error);
532
+ res.status(500).json({ error: "Failed to load project info" });
533
+ }
534
+ });
535
+ return router;
536
+ }
537
+
538
+ // src/server/routes/commits.ts
539
+ import { Router as Router3 } from "express";
540
+
541
+ // src/server/loaders/git.ts
542
+ import simpleGit from "simple-git";
543
+ import * as path2 from "path";
544
+ function extractSymbolsFromFiles(files) {
545
+ const symbols = /* @__PURE__ */ new Set();
546
+ for (const file of files) {
547
+ if (file.endsWith(".purpose")) {
548
+ const dir = path2.dirname(file);
549
+ const name = path2.basename(dir);
550
+ if (dir.includes("features/") || dir.includes("routes/") || dir.includes("api/")) {
551
+ symbols.add(`@${name}`);
552
+ } else if (dir.includes("components/") || dir.includes("lib/") || dir.includes("utils/")) {
553
+ symbols.add(`#${name}`);
554
+ } else if (dir.includes("middleware/") || dir.includes("auth/") || dir.includes("guards/")) {
555
+ symbols.add(`^${name}`);
556
+ } else if (dir.includes("flows/") || dir.includes("workflows/")) {
557
+ symbols.add(`$${name}`);
558
+ }
559
+ }
560
+ if (file.includes("portal.yaml")) {
561
+ symbols.add("^portal");
562
+ }
563
+ const featureMatch = file.match(/features\/([^/]+)/);
564
+ if (featureMatch) {
565
+ symbols.add(`@${featureMatch[1]}`);
566
+ }
567
+ const componentMatch = file.match(/components\/([^/]+)/);
568
+ if (componentMatch) {
569
+ symbols.add(`#${componentMatch[1]}`);
570
+ }
571
+ }
572
+ return Array.from(symbols);
573
+ }
574
+ async function loadGitHistory(projectDir, options = {}) {
575
+ const git = simpleGit(projectDir);
576
+ const isRepo = await git.checkIsRepo();
577
+ if (!isRepo) {
578
+ return [];
579
+ }
580
+ try {
581
+ const logOptions = {
582
+ maxCount: options.limit || 100
583
+ };
584
+ if (options.since) {
585
+ logOptions["--since"] = options.since;
586
+ }
587
+ const log4 = await git.log(logOptions);
588
+ const commits = [];
589
+ for (const commit of log4.all) {
590
+ let filesChanged = [];
591
+ let symbolsModified = [];
592
+ try {
593
+ const diff = await git.diffSummary([`${commit.hash}^`, commit.hash]);
594
+ filesChanged = diff.files.map((f) => f.file);
595
+ symbolsModified = extractSymbolsFromFiles(filesChanged);
596
+ } catch {
597
+ }
598
+ commits.push({
599
+ hash: commit.hash,
600
+ shortHash: commit.hash.slice(0, 7),
601
+ date: commit.date,
602
+ author: commit.author_name,
603
+ message: commit.message.split("\n")[0],
604
+ // First line only
605
+ symbolsModified,
606
+ filesChanged
607
+ });
608
+ }
609
+ return commits;
610
+ } catch (error) {
611
+ console.error("Failed to load git history:", error);
612
+ return [];
613
+ }
614
+ }
615
+ async function getSymbolsAtCommit(projectDir, commitHash) {
616
+ const git = simpleGit(projectDir);
617
+ try {
618
+ const result = await git.raw(["ls-tree", "-r", "--name-only", commitHash]);
619
+ const files = result.split("\n").filter(Boolean);
620
+ const purposeFiles = files.filter((f) => f.endsWith(".purpose"));
621
+ return extractSymbolsFromFiles(purposeFiles);
622
+ } catch (error) {
623
+ console.error("Failed to get symbols at commit:", error);
624
+ return [];
625
+ }
626
+ }
627
+
628
+ // src/server/routes/commits.ts
629
+ function createCommitsRouter(projectDir) {
630
+ const router = Router3();
631
+ router.get("/", async (req, res) => {
632
+ try {
633
+ const limit = parseInt(req.query.limit) || 100;
634
+ const since = req.query.since;
635
+ const commits = await loadGitHistory(projectDir, { limit, since });
636
+ res.json({ commits });
637
+ } catch (error) {
638
+ console.error("Failed to load commits:", error);
639
+ res.status(500).json({ error: "Failed to load commits" });
640
+ }
641
+ });
642
+ return router;
643
+ }
644
+
645
+ // src/server/routes/incidents.ts
646
+ import { Router as Router4 } from "express";
647
+ function createIncidentsRouter(_projectDir) {
648
+ const router = Router4();
649
+ const storage = new SentinelStorage();
650
+ router.get("/", async (req, res) => {
651
+ try {
652
+ const limit = parseInt(req.query.limit) || 50;
653
+ const status = req.query.status;
654
+ const environment = req.query.environment;
655
+ const symbol = req.query.symbol;
656
+ const options = { limit };
657
+ if (status && ["open", "investigating", "resolved", "wont-fix"].includes(status)) {
658
+ options.status = status;
659
+ }
660
+ if (environment) options.environment = environment;
661
+ if (symbol) options.symbol = symbol;
662
+ const incidents = storage.getRecentIncidents(options);
663
+ const summaries = incidents.map((incident) => ({
664
+ id: incident.id,
665
+ timestamp: incident.timestamp,
666
+ status: incident.status,
667
+ error: {
668
+ message: incident.error.message,
669
+ type: incident.error.type
670
+ },
671
+ symbols: incident.symbols,
672
+ environment: incident.environment,
673
+ patternMatches: []
674
+ // Would need PatternMatcher to populate
675
+ }));
676
+ res.json({ incidents: summaries });
677
+ } catch (error) {
678
+ console.error("Failed to load incidents:", error);
679
+ res.status(500).json({ error: "Failed to load incidents" });
680
+ }
681
+ });
682
+ router.get("/:id", async (req, res) => {
683
+ try {
684
+ const incident = storage.getIncident(req.params.id);
685
+ if (!incident) {
686
+ res.status(404).json({ error: "Incident not found" });
687
+ return;
688
+ }
689
+ res.json({ incident });
690
+ } catch (error) {
691
+ console.error("Failed to load incident:", error);
692
+ res.status(500).json({ error: "Failed to load incident" });
693
+ }
694
+ });
695
+ router.post("/:id/resolve", async (req, res) => {
696
+ try {
697
+ const incident = storage.getIncident(req.params.id);
698
+ if (!incident) {
699
+ res.status(404).json({ error: "Incident not found" });
700
+ return;
701
+ }
702
+ storage.resolveIncident(req.params.id, {
703
+ notes: req.body.notes,
704
+ patternId: req.body.patternId
705
+ });
706
+ res.json({ success: true });
707
+ } catch (error) {
708
+ console.error("Failed to resolve incident:", error);
709
+ res.status(500).json({ error: "Failed to resolve incident" });
710
+ }
711
+ });
712
+ return router;
713
+ }
714
+
715
+ // src/server/routes/patterns.ts
716
+ import { Router as Router5 } from "express";
717
+ function createPatternsRouter(_projectDir) {
718
+ const router = Router5();
719
+ const storage = new SentinelStorage();
720
+ router.get("/", async (req, res) => {
721
+ try {
722
+ const source = req.query.source;
723
+ const symbol = req.query.symbol;
724
+ const minConfidence = parseInt(req.query.minConfidence) || void 0;
725
+ const options = {};
726
+ if (source && ["manual", "suggested", "imported", "community"].includes(source)) {
727
+ options.source = source;
728
+ }
729
+ if (symbol) options.symbol = symbol;
730
+ if (minConfidence) options.minConfidence = minConfidence;
731
+ const patterns = storage.getAllPatterns(options);
732
+ const summaries = patterns.map((pattern) => ({
733
+ id: pattern.id,
734
+ name: pattern.name,
735
+ description: pattern.description,
736
+ confidence: {
737
+ score: pattern.confidence.score,
738
+ timesMatched: pattern.confidence.timesMatched,
739
+ timesResolved: pattern.confidence.timesResolved
740
+ },
741
+ tags: pattern.tags
742
+ }));
743
+ res.json({ patterns: summaries });
744
+ } catch (error) {
745
+ console.error("Failed to load patterns:", error);
746
+ res.status(500).json({ error: "Failed to load patterns" });
747
+ }
748
+ });
749
+ router.get("/:id", async (req, res) => {
750
+ try {
751
+ const pattern = storage.getPattern(req.params.id);
752
+ if (!pattern) {
753
+ res.status(404).json({ error: "Pattern not found" });
754
+ return;
755
+ }
756
+ res.json({ pattern });
757
+ } catch (error) {
758
+ console.error("Failed to load pattern:", error);
759
+ res.status(500).json({ error: "Failed to load pattern" });
760
+ }
761
+ });
762
+ return router;
763
+ }
764
+
765
+ // src/server/index.ts
766
+ var __filename = fileURLToPath(import.meta.url);
767
+ var __dirname = path3.dirname(__filename);
768
+ var log3 = {
769
+ component(name) {
770
+ const symbol = chalk3.magenta(`#${name}`);
771
+ return {
772
+ info: (msg, data) => {
773
+ const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
774
+ console.log(`${chalk3.blue("\u2139")} ${symbol} ${msg}${dataStr}`);
775
+ },
776
+ success: (msg, data) => {
777
+ const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
778
+ console.log(`${chalk3.green("\u2714")} ${symbol} ${msg}${dataStr}`);
779
+ },
780
+ warn: (msg, data) => {
781
+ const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
782
+ console.log(`${chalk3.yellow("\u26A0")} ${symbol} ${msg}${dataStr}`);
783
+ },
784
+ error: (msg, data) => {
785
+ const dataStr = data ? chalk3.gray(` ${Object.entries(data).map(([k, v]) => `${k}=${v}`).join(" ")}`) : "";
786
+ console.error(`${chalk3.red("\u2716")} ${symbol} ${msg}${dataStr}`);
787
+ }
788
+ };
789
+ }
790
+ };
791
+ function createApp(options) {
792
+ const app = express();
793
+ app.use(express.json());
794
+ app.use((_req, res, next) => {
795
+ res.header("Access-Control-Allow-Origin", "*");
796
+ res.header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS");
797
+ res.header("Access-Control-Allow-Headers", "Content-Type");
798
+ next();
799
+ });
800
+ app.use("/api/symbols", createSymbolsRouter(options.projectDir));
801
+ app.use("/api/info", createInfoRouter(options.projectDir));
802
+ app.use("/api/commits", createCommitsRouter(options.projectDir));
803
+ app.use("/api/incidents", createIncidentsRouter(options.projectDir));
804
+ app.use("/api/patterns", createPatternsRouter(options.projectDir));
805
+ app.get("/api/health", (_req, res) => {
806
+ res.json({ status: "ok", timestamp: (/* @__PURE__ */ new Date()).toISOString() });
807
+ });
808
+ const uiDistPath = path3.join(__dirname, "..", "..", "ui", "dist");
809
+ if (fs2.existsSync(uiDistPath)) {
810
+ app.use(express.static(uiDistPath));
811
+ app.get("{*path}", (req, res) => {
812
+ if (!req.path.startsWith("/api")) {
813
+ res.sendFile(path3.join(uiDistPath, "index.html"));
814
+ }
815
+ });
816
+ }
817
+ return app;
818
+ }
819
+ async function startServer(options) {
820
+ const app = createApp(options);
821
+ log3.component("sentinel-server").info("Starting server", { port: options.port });
822
+ log3.component("sentinel-server").info("Project directory", { path: options.projectDir });
823
+ return new Promise((resolve, reject) => {
824
+ const server = app.listen(options.port, () => {
825
+ log3.component("sentinel-server").success("Server running", { url: `http://localhost:${options.port}` });
826
+ if (options.open) {
827
+ import("open").then((openModule) => {
828
+ openModule.default(`http://localhost:${options.port}`);
829
+ log3.component("sentinel-server").info("Opened browser");
830
+ }).catch(() => {
831
+ log3.component("sentinel-server").warn("Could not open browser automatically");
832
+ });
833
+ }
834
+ resolve();
835
+ });
836
+ server.on("error", (err) => {
837
+ if (err.code === "EADDRINUSE") {
838
+ log3.component("sentinel-server").error("Port already in use", { port: options.port });
839
+ } else {
840
+ log3.component("sentinel-server").error("Server error", { error: err.message });
841
+ }
842
+ reject(err);
843
+ });
844
+ });
845
+ }
846
+ export {
847
+ createApp,
848
+ getSymbolCount,
849
+ getSymbolsAtCommit,
850
+ loadGitHistory,
851
+ loadParadigmConfig,
852
+ loadSymbolIndex,
853
+ startServer
854
+ };