@sourcepress/server 0.1.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.
- package/.omc/state/agent-replay-31d84b63-606a-4368-b9e6-93fe4f5ae0f7.jsonl +2 -0
- package/.omc/state/last-tool-error.json +7 -0
- package/.omc/state/mission-state.json +53 -0
- package/.omc/state/subagent-tracking.json +17 -0
- package/.turbo/turbo-build.log +4 -0
- package/.turbo/turbo-test.log +26 -0
- package/dist/__tests__/app-integration.test.d.ts +2 -0
- package/dist/__tests__/app-integration.test.d.ts.map +1 -0
- package/dist/__tests__/app-integration.test.js +71 -0
- package/dist/__tests__/app-integration.test.js.map +1 -0
- package/dist/__tests__/approval.test.d.ts +2 -0
- package/dist/__tests__/approval.test.d.ts.map +1 -0
- package/dist/__tests__/approval.test.js +170 -0
- package/dist/__tests__/approval.test.js.map +1 -0
- package/dist/__tests__/content.test.d.ts +2 -0
- package/dist/__tests__/content.test.d.ts.map +1 -0
- package/dist/__tests__/content.test.js +187 -0
- package/dist/__tests__/content.test.js.map +1 -0
- package/dist/__tests__/engine.test.d.ts +2 -0
- package/dist/__tests__/engine.test.d.ts.map +1 -0
- package/dist/__tests__/engine.test.js +77 -0
- package/dist/__tests__/engine.test.js.map +1 -0
- package/dist/__tests__/eval.test.d.ts +2 -0
- package/dist/__tests__/eval.test.d.ts.map +1 -0
- package/dist/__tests__/eval.test.js +320 -0
- package/dist/__tests__/eval.test.js.map +1 -0
- package/dist/__tests__/graph.test.d.ts +2 -0
- package/dist/__tests__/graph.test.d.ts.map +1 -0
- package/dist/__tests__/graph.test.js +169 -0
- package/dist/__tests__/graph.test.js.map +1 -0
- package/dist/__tests__/health.test.d.ts +2 -0
- package/dist/__tests__/health.test.d.ts.map +1 -0
- package/dist/__tests__/health.test.js +56 -0
- package/dist/__tests__/health.test.js.map +1 -0
- package/dist/__tests__/import.test.d.ts +2 -0
- package/dist/__tests__/import.test.d.ts.map +1 -0
- package/dist/__tests__/import.test.js +138 -0
- package/dist/__tests__/import.test.js.map +1 -0
- package/dist/__tests__/intent.test.d.ts +2 -0
- package/dist/__tests__/intent.test.d.ts.map +1 -0
- package/dist/__tests__/intent.test.js +122 -0
- package/dist/__tests__/intent.test.js.map +1 -0
- package/dist/__tests__/jobs.test.d.ts +2 -0
- package/dist/__tests__/jobs.test.d.ts.map +1 -0
- package/dist/__tests__/jobs.test.js +96 -0
- package/dist/__tests__/jobs.test.js.map +1 -0
- package/dist/__tests__/knowledge.test.d.ts +2 -0
- package/dist/__tests__/knowledge.test.d.ts.map +1 -0
- package/dist/__tests__/knowledge.test.js +110 -0
- package/dist/__tests__/knowledge.test.js.map +1 -0
- package/dist/__tests__/media-routes.test.d.ts +2 -0
- package/dist/__tests__/media-routes.test.d.ts.map +1 -0
- package/dist/__tests__/media-routes.test.js +88 -0
- package/dist/__tests__/media-routes.test.js.map +1 -0
- package/dist/__tests__/schema.test.d.ts +2 -0
- package/dist/__tests__/schema.test.d.ts.map +1 -0
- package/dist/__tests__/schema.test.js +92 -0
- package/dist/__tests__/schema.test.js.map +1 -0
- package/dist/app.d.ts +7 -0
- package/dist/app.d.ts.map +1 -0
- package/dist/app.js +85 -0
- package/dist/app.js.map +1 -0
- package/dist/engine.d.ts +38 -0
- package/dist/engine.d.ts.map +1 -0
- package/dist/engine.js +106 -0
- package/dist/engine.js.map +1 -0
- package/dist/index.d.ts +8 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +9 -0
- package/dist/index.js.map +1 -0
- package/dist/middleware/auth.d.ts +17 -0
- package/dist/middleware/auth.d.ts.map +1 -0
- package/dist/middleware/auth.js +54 -0
- package/dist/middleware/auth.js.map +1 -0
- package/dist/middleware/cors.d.ts +2 -0
- package/dist/middleware/cors.d.ts.map +1 -0
- package/dist/middleware/cors.js +13 -0
- package/dist/middleware/cors.js.map +1 -0
- package/dist/middleware/error-handler.d.ts +24 -0
- package/dist/middleware/error-handler.d.ts.map +1 -0
- package/dist/middleware/error-handler.js +30 -0
- package/dist/middleware/error-handler.js.map +1 -0
- package/dist/middleware/index.d.ts +7 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +5 -0
- package/dist/middleware/index.js.map +1 -0
- package/dist/middleware/rate-limit.d.ts +11 -0
- package/dist/middleware/rate-limit.d.ts.map +1 -0
- package/dist/middleware/rate-limit.js +26 -0
- package/dist/middleware/rate-limit.js.map +1 -0
- package/dist/middleware/route-error-handler.d.ts +12 -0
- package/dist/middleware/route-error-handler.d.ts.map +1 -0
- package/dist/middleware/route-error-handler.js +9 -0
- package/dist/middleware/route-error-handler.js.map +1 -0
- package/dist/routes/approval.d.ts +4 -0
- package/dist/routes/approval.d.ts.map +1 -0
- package/dist/routes/approval.js +70 -0
- package/dist/routes/approval.js.map +1 -0
- package/dist/routes/content.d.ts +4 -0
- package/dist/routes/content.d.ts.map +1 -0
- package/dist/routes/content.js +145 -0
- package/dist/routes/content.js.map +1 -0
- package/dist/routes/eval.d.ts +4 -0
- package/dist/routes/eval.d.ts.map +1 -0
- package/dist/routes/eval.js +178 -0
- package/dist/routes/eval.js.map +1 -0
- package/dist/routes/graph.d.ts +4 -0
- package/dist/routes/graph.d.ts.map +1 -0
- package/dist/routes/graph.js +90 -0
- package/dist/routes/graph.js.map +1 -0
- package/dist/routes/health.d.ts +13 -0
- package/dist/routes/health.d.ts.map +1 -0
- package/dist/routes/health.js +19 -0
- package/dist/routes/health.js.map +1 -0
- package/dist/routes/import.d.ts +4 -0
- package/dist/routes/import.d.ts.map +1 -0
- package/dist/routes/import.js +85 -0
- package/dist/routes/import.js.map +1 -0
- package/dist/routes/index.d.ts +12 -0
- package/dist/routes/index.d.ts.map +1 -0
- package/dist/routes/index.js +12 -0
- package/dist/routes/index.js.map +1 -0
- package/dist/routes/intent.d.ts +4 -0
- package/dist/routes/intent.d.ts.map +1 -0
- package/dist/routes/intent.js +80 -0
- package/dist/routes/intent.js.map +1 -0
- package/dist/routes/jobs.d.ts +4 -0
- package/dist/routes/jobs.d.ts.map +1 -0
- package/dist/routes/jobs.js +67 -0
- package/dist/routes/jobs.js.map +1 -0
- package/dist/routes/knowledge.d.ts +4 -0
- package/dist/routes/knowledge.d.ts.map +1 -0
- package/dist/routes/knowledge.js +48 -0
- package/dist/routes/knowledge.js.map +1 -0
- package/dist/routes/media.d.ts +4 -0
- package/dist/routes/media.d.ts.map +1 -0
- package/dist/routes/media.js +87 -0
- package/dist/routes/media.js.map +1 -0
- package/dist/routes/schema.d.ts +4 -0
- package/dist/routes/schema.d.ts.map +1 -0
- package/dist/routes/schema.js +54 -0
- package/dist/routes/schema.js.map +1 -0
- package/dist/standalone.d.ts +2 -0
- package/dist/standalone.d.ts.map +1 -0
- package/dist/standalone.js +115 -0
- package/dist/standalone.js.map +1 -0
- package/package.json +36 -0
- package/src/__tests__/app-integration.test.ts +80 -0
- package/src/__tests__/approval.test.ts +195 -0
- package/src/__tests__/content.test.ts +202 -0
- package/src/__tests__/engine.test.ts +86 -0
- package/src/__tests__/eval.test.ts +343 -0
- package/src/__tests__/graph.test.ts +182 -0
- package/src/__tests__/health.test.ts +68 -0
- package/src/__tests__/import.test.ts +148 -0
- package/src/__tests__/intent.test.ts +133 -0
- package/src/__tests__/jobs.test.ts +107 -0
- package/src/__tests__/knowledge.test.ts +121 -0
- package/src/__tests__/media-routes.test.ts +109 -0
- package/src/__tests__/schema.test.ts +100 -0
- package/src/app.ts +92 -0
- package/src/engine.ts +168 -0
- package/src/index.ts +31 -0
- package/src/middleware/auth.ts +66 -0
- package/src/middleware/cors.ts +15 -0
- package/src/middleware/error-handler.ts +42 -0
- package/src/middleware/index.ts +6 -0
- package/src/middleware/rate-limit.ts +27 -0
- package/src/middleware/route-error-handler.ts +13 -0
- package/src/routes/approval.ts +90 -0
- package/src/routes/content.ts +256 -0
- package/src/routes/eval.ts +262 -0
- package/src/routes/graph.ts +111 -0
- package/src/routes/health.ts +33 -0
- package/src/routes/import.ts +122 -0
- package/src/routes/index.ts +11 -0
- package/src/routes/intent.ts +105 -0
- package/src/routes/jobs.ts +84 -0
- package/src/routes/knowledge.ts +73 -0
- package/src/routes/media.ts +117 -0
- package/src/routes/schema.ts +75 -0
- package/src/standalone.ts +130 -0
- package/tsconfig.json +8 -0
- package/vitest.config.ts +7 -0
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import { EvalRunner, judge } from "@sourcepress/ai";
|
|
2
|
+
import type { EvalRunConfig } from "@sourcepress/ai";
|
|
3
|
+
import { Hono } from "hono";
|
|
4
|
+
import type { EngineContext } from "../engine.js";
|
|
5
|
+
import { SourcePressError } from "../middleware/error-handler.js";
|
|
6
|
+
import { handleRouteError } from "../middleware/route-error-handler.js";
|
|
7
|
+
|
|
8
|
+
export function evalRoutes(engine: EngineContext) {
|
|
9
|
+
const app = new Hono();
|
|
10
|
+
|
|
11
|
+
app.onError(handleRouteError);
|
|
12
|
+
|
|
13
|
+
// POST /eval/judge — single judge pass on content
|
|
14
|
+
app.post("/eval/judge", async (c) => {
|
|
15
|
+
const body = await c.req.json<{
|
|
16
|
+
draft: string;
|
|
17
|
+
gold_standard: string;
|
|
18
|
+
judge_prompt: string;
|
|
19
|
+
intent?: string;
|
|
20
|
+
}>();
|
|
21
|
+
|
|
22
|
+
if (!body.draft || !body.gold_standard || !body.judge_prompt) {
|
|
23
|
+
throw new SourcePressError(
|
|
24
|
+
400,
|
|
25
|
+
"INVALID_INPUT",
|
|
26
|
+
"draft, gold_standard, and judge_prompt are required",
|
|
27
|
+
);
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
const result = await judge(
|
|
31
|
+
{
|
|
32
|
+
draft: body.draft,
|
|
33
|
+
gold_standard: body.gold_standard,
|
|
34
|
+
judge_prompt: body.judge_prompt,
|
|
35
|
+
intent: body.intent,
|
|
36
|
+
},
|
|
37
|
+
engine.provider,
|
|
38
|
+
engine.budget,
|
|
39
|
+
);
|
|
40
|
+
|
|
41
|
+
return c.json({
|
|
42
|
+
score: result.score,
|
|
43
|
+
reasoning: result.reasoning,
|
|
44
|
+
usage: result.usage,
|
|
45
|
+
});
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
// POST /eval/run — full generate-judge-improve loop
|
|
49
|
+
app.post("/eval/run", async (c) => {
|
|
50
|
+
const body = await c.req.json<{
|
|
51
|
+
content_type: string;
|
|
52
|
+
knowledge_context: string;
|
|
53
|
+
gold_standard: string;
|
|
54
|
+
judge_prompt: string;
|
|
55
|
+
generation_prompt: string;
|
|
56
|
+
intent?: string;
|
|
57
|
+
threshold?: number;
|
|
58
|
+
max_iterations?: number;
|
|
59
|
+
collection_schema?: Record<string, unknown>;
|
|
60
|
+
}>();
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
!body.content_type ||
|
|
64
|
+
!body.knowledge_context ||
|
|
65
|
+
!body.gold_standard ||
|
|
66
|
+
!body.judge_prompt ||
|
|
67
|
+
!body.generation_prompt
|
|
68
|
+
) {
|
|
69
|
+
throw new SourcePressError(
|
|
70
|
+
400,
|
|
71
|
+
"INVALID_INPUT",
|
|
72
|
+
"content_type, knowledge_context, gold_standard, judge_prompt, and generation_prompt are required",
|
|
73
|
+
);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const threshold =
|
|
77
|
+
body.threshold ??
|
|
78
|
+
(engine.config.evals as { threshold?: number } | undefined)?.threshold ??
|
|
79
|
+
70;
|
|
80
|
+
const maxIterations = body.max_iterations ?? 3;
|
|
81
|
+
|
|
82
|
+
const config: EvalRunConfig = {
|
|
83
|
+
content_type: body.content_type,
|
|
84
|
+
knowledge_context: body.knowledge_context,
|
|
85
|
+
gold_standard: body.gold_standard,
|
|
86
|
+
judge_prompt: body.judge_prompt,
|
|
87
|
+
generation_prompt: body.generation_prompt,
|
|
88
|
+
intent: body.intent,
|
|
89
|
+
threshold,
|
|
90
|
+
max_iterations: maxIterations,
|
|
91
|
+
collection_schema: body.collection_schema,
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const runner = new EvalRunner(engine.provider, engine.budget);
|
|
95
|
+
const result = await runner.run(config);
|
|
96
|
+
|
|
97
|
+
// Log result to results.tsv
|
|
98
|
+
try {
|
|
99
|
+
const resultsPath = "evals/results.tsv";
|
|
100
|
+
const existing = await engine.github.getFile(resultsPath);
|
|
101
|
+
const header = "date\tcontent_type\tprompt_version\tscore\tstatus\treasoning";
|
|
102
|
+
const row = `${new Date().toISOString()}\t${body.content_type}\tdefault\t${result.final_score}\t${result.final_status}\t${(result.iterations[result.iterations.length - 1]?.reasoning ?? "").replace(/\t/g, " ")}`;
|
|
103
|
+
const content = existing ? `${existing.content.trimEnd()}\n${row}\n` : `${header}\n${row}\n`;
|
|
104
|
+
await engine.github.createOrUpdateFile(
|
|
105
|
+
resultsPath,
|
|
106
|
+
content,
|
|
107
|
+
"eval: log result",
|
|
108
|
+
undefined,
|
|
109
|
+
existing?.sha,
|
|
110
|
+
);
|
|
111
|
+
} catch {
|
|
112
|
+
// Non-critical — don't fail the eval if logging fails
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return c.json(result);
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
// GET /eval/results — list eval results from evals/results.tsv in the repo
|
|
119
|
+
app.get("/eval/results", async (c) => {
|
|
120
|
+
const resultsPath = "evals/results.tsv";
|
|
121
|
+
const file = await engine.github.getFile(resultsPath);
|
|
122
|
+
|
|
123
|
+
if (!file) {
|
|
124
|
+
return c.json({ items: [], total: 0 });
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const lines = file.content.trim().split("\n");
|
|
128
|
+
if (lines.length <= 1) {
|
|
129
|
+
return c.json({ items: [], total: 0 });
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Parse TSV: date, content_type, prompt_version, score, status, reasoning
|
|
133
|
+
const headers = lines[0].split("\t");
|
|
134
|
+
const items = lines.slice(1).map((line) => {
|
|
135
|
+
const values = line.split("\t");
|
|
136
|
+
const obj: Record<string, string> = {};
|
|
137
|
+
for (let i = 0; i < headers.length; i++) {
|
|
138
|
+
obj[headers[i].trim()] = values[i]?.trim() ?? "";
|
|
139
|
+
}
|
|
140
|
+
return obj;
|
|
141
|
+
});
|
|
142
|
+
|
|
143
|
+
// Filter by content_type if provided
|
|
144
|
+
const contentType = c.req.query("content_type");
|
|
145
|
+
const filtered = contentType
|
|
146
|
+
? items.filter((item) => item.content_type === contentType)
|
|
147
|
+
: items;
|
|
148
|
+
|
|
149
|
+
return c.json({ items: filtered, total: filtered.length });
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
// GET /eval/prompts — list generation prompts from evals/prompts/ in the repo
|
|
153
|
+
app.get("/eval/prompts", async (c) => {
|
|
154
|
+
const tree = await engine.github.getTree();
|
|
155
|
+
const promptFiles = tree
|
|
156
|
+
.filter((entry) => entry.path.startsWith("evals/prompts/") && entry.type === "blob")
|
|
157
|
+
.map((entry) => ({
|
|
158
|
+
path: entry.path,
|
|
159
|
+
name: entry.path.replace("evals/prompts/", "").replace(/\.(md|txt)$/, ""),
|
|
160
|
+
}));
|
|
161
|
+
|
|
162
|
+
return c.json({ items: promptFiles, total: promptFiles.length });
|
|
163
|
+
});
|
|
164
|
+
|
|
165
|
+
// GET /eval/prompts/:name — get a single generation prompt
|
|
166
|
+
app.get("/eval/prompts/:name", async (c) => {
|
|
167
|
+
const name = c.req.param("name");
|
|
168
|
+
const filePath = `evals/prompts/${name}.md`;
|
|
169
|
+
const file = await engine.github.getFile(filePath);
|
|
170
|
+
|
|
171
|
+
if (!file) {
|
|
172
|
+
throw new SourcePressError(404, "PROMPT_NOT_FOUND", `Generation prompt "${name}" not found`);
|
|
173
|
+
}
|
|
174
|
+
|
|
175
|
+
return c.json({
|
|
176
|
+
name,
|
|
177
|
+
path: file.path,
|
|
178
|
+
content: file.content,
|
|
179
|
+
});
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
// PUT /eval/prompts/:name — update a generation prompt (direct commit, self-improving)
|
|
183
|
+
app.put("/eval/prompts/:name", async (c) => {
|
|
184
|
+
const name = c.req.param("name");
|
|
185
|
+
const filePath = `evals/prompts/${name}.md`;
|
|
186
|
+
const body = await c.req.json<{ content: string }>();
|
|
187
|
+
|
|
188
|
+
if (!body.content) {
|
|
189
|
+
throw new SourcePressError(400, "INVALID_INPUT", "content is required");
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
const existing = await engine.github.getFile(filePath);
|
|
193
|
+
const existingSha = existing?.sha;
|
|
194
|
+
|
|
195
|
+
const result = await engine.github.createOrUpdateFile(
|
|
196
|
+
filePath,
|
|
197
|
+
body.content,
|
|
198
|
+
`update(eval): improve ${name} generation prompt`,
|
|
199
|
+
undefined,
|
|
200
|
+
existingSha,
|
|
201
|
+
);
|
|
202
|
+
|
|
203
|
+
return c.json({
|
|
204
|
+
updated: true,
|
|
205
|
+
path: filePath,
|
|
206
|
+
commit_sha: result.commit_sha,
|
|
207
|
+
});
|
|
208
|
+
});
|
|
209
|
+
|
|
210
|
+
// GET /eval/standards — list gold standard files
|
|
211
|
+
app.get("/eval/standards", async (c) => {
|
|
212
|
+
const tree = await engine.github.getTree();
|
|
213
|
+
const standardFiles = tree
|
|
214
|
+
.filter((entry) => entry.path.startsWith("evals/standards/") && entry.type === "blob")
|
|
215
|
+
.map((entry) => ({
|
|
216
|
+
path: entry.path,
|
|
217
|
+
name: entry.path.replace("evals/standards/", "").replace(/\.(mdx|md)$/, ""),
|
|
218
|
+
}));
|
|
219
|
+
|
|
220
|
+
return c.json({ items: standardFiles, total: standardFiles.length });
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
// GET /eval/standards/:name — get a single gold standard
|
|
224
|
+
app.get("/eval/standards/:name", async (c) => {
|
|
225
|
+
const name = c.req.param("name");
|
|
226
|
+
// Try .mdx first, then .md
|
|
227
|
+
let file = await engine.github.getFile(`evals/standards/${name}.mdx`);
|
|
228
|
+
if (!file) {
|
|
229
|
+
file = await engine.github.getFile(`evals/standards/${name}.md`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (!file) {
|
|
233
|
+
throw new SourcePressError(404, "STANDARD_NOT_FOUND", `Gold standard "${name}" not found`);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
return c.json({
|
|
237
|
+
name,
|
|
238
|
+
path: file.path,
|
|
239
|
+
content: file.content,
|
|
240
|
+
});
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
// GET /eval/judge-prompt — get the judge.md prompt
|
|
244
|
+
app.get("/eval/judge-prompt", async (c) => {
|
|
245
|
+
const file = await engine.github.getFile("evals/judge.md");
|
|
246
|
+
|
|
247
|
+
if (!file) {
|
|
248
|
+
throw new SourcePressError(
|
|
249
|
+
404,
|
|
250
|
+
"JUDGE_PROMPT_NOT_FOUND",
|
|
251
|
+
"evals/judge.md not found in repository",
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
return c.json({
|
|
256
|
+
path: file.path,
|
|
257
|
+
content: file.content,
|
|
258
|
+
});
|
|
259
|
+
});
|
|
260
|
+
|
|
261
|
+
return app;
|
|
262
|
+
}
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { EngineContext } from "../engine.js";
|
|
3
|
+
import { SourcePressError } from "../middleware/error-handler.js";
|
|
4
|
+
import { handleRouteError } from "../middleware/route-error-handler.js";
|
|
5
|
+
|
|
6
|
+
export function graphRoutes(engine: EngineContext) {
|
|
7
|
+
const app = new Hono();
|
|
8
|
+
|
|
9
|
+
app.onError(handleRouteError);
|
|
10
|
+
|
|
11
|
+
// GET /graph — get full graph summary
|
|
12
|
+
app.get("/graph", async (c) => {
|
|
13
|
+
const graph = engine.knowledge.getGraph();
|
|
14
|
+
|
|
15
|
+
if (!graph) {
|
|
16
|
+
return c.json({
|
|
17
|
+
status: "empty",
|
|
18
|
+
message: "No graph built yet. POST /api/graph/rebuild to build.",
|
|
19
|
+
entities: 0,
|
|
20
|
+
relations: 0,
|
|
21
|
+
clusters: 0,
|
|
22
|
+
});
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
// Serialize Map to array for JSON
|
|
26
|
+
const entities = Array.from(graph.entities.values()).map((e) => ({
|
|
27
|
+
name: e.name,
|
|
28
|
+
type: e.type,
|
|
29
|
+
aliases: e.aliases,
|
|
30
|
+
confidence: e.confidence,
|
|
31
|
+
source_file: e.source_file,
|
|
32
|
+
}));
|
|
33
|
+
|
|
34
|
+
return c.json({
|
|
35
|
+
status: "built",
|
|
36
|
+
built_at: graph.built_at,
|
|
37
|
+
file_count: graph.file_count,
|
|
38
|
+
entity_count: entities.length,
|
|
39
|
+
relation_count: graph.relations.length,
|
|
40
|
+
cluster_count: graph.clusters.length,
|
|
41
|
+
entities,
|
|
42
|
+
relations: graph.relations,
|
|
43
|
+
clusters: graph.clusters,
|
|
44
|
+
});
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
// GET /graph/entity/:name — query a specific entity
|
|
48
|
+
app.get("/graph/entity/:name", async (c) => {
|
|
49
|
+
const name = decodeURIComponent(c.req.param("name"));
|
|
50
|
+
const result = engine.knowledge.query(name);
|
|
51
|
+
|
|
52
|
+
if (!result) {
|
|
53
|
+
throw new SourcePressError(
|
|
54
|
+
404,
|
|
55
|
+
"ENTITY_NOT_FOUND",
|
|
56
|
+
`Entity "${name}" not found in graph. Graph may need rebuilding.`,
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
return c.json(result);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
// GET /graph/stale — find stale content
|
|
64
|
+
app.get("/graph/stale", async (c) => {
|
|
65
|
+
const collections = engine.listCollections();
|
|
66
|
+
const allContent = [];
|
|
67
|
+
for (const coll of collections) {
|
|
68
|
+
const files = await engine.listContent(coll);
|
|
69
|
+
allContent.push(...files);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Build timestamps from knowledge store
|
|
73
|
+
const knowledgeFiles = await engine.knowledgeStore.list();
|
|
74
|
+
const timestamps: Record<string, string> = {};
|
|
75
|
+
for (const f of knowledgeFiles) {
|
|
76
|
+
timestamps[f.path] = f.ingested_at;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const stale = engine.knowledge.findStale(allContent, timestamps);
|
|
80
|
+
return c.json({ items: stale, total: stale.length });
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
// GET /graph/gaps — find knowledge gaps
|
|
84
|
+
app.get("/graph/gaps", async (c) => {
|
|
85
|
+
const collections = engine.listCollections();
|
|
86
|
+
const allContent = [];
|
|
87
|
+
for (const coll of collections) {
|
|
88
|
+
const files = await engine.listContent(coll);
|
|
89
|
+
allContent.push(...files);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const gaps = engine.knowledge.findGaps(allContent);
|
|
93
|
+
return c.json({ items: gaps, total: gaps.length });
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
// POST /graph/rebuild — rebuild the knowledge graph
|
|
97
|
+
app.post("/graph/rebuild", async (c) => {
|
|
98
|
+
const graph = await engine.knowledge.buildGraph();
|
|
99
|
+
|
|
100
|
+
return c.json({
|
|
101
|
+
rebuilt: true,
|
|
102
|
+
built_at: graph.built_at,
|
|
103
|
+
entity_count: graph.entities.size,
|
|
104
|
+
relation_count: graph.relations.length,
|
|
105
|
+
cluster_count: graph.clusters.length,
|
|
106
|
+
file_count: graph.file_count,
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
return app;
|
|
111
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
|
|
3
|
+
export interface HealthStatus {
|
|
4
|
+
status: "ok" | "degraded" | "error";
|
|
5
|
+
version: string;
|
|
6
|
+
timestamp: string;
|
|
7
|
+
services: {
|
|
8
|
+
github: "connected" | "disconnected" | "unknown";
|
|
9
|
+
knowledge: "ready" | "empty" | "unknown";
|
|
10
|
+
cache: "ready" | "unknown";
|
|
11
|
+
};
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function healthRoutes() {
|
|
15
|
+
const app = new Hono();
|
|
16
|
+
|
|
17
|
+
app.get("/health", (c) => {
|
|
18
|
+
const health: HealthStatus = {
|
|
19
|
+
status: "ok",
|
|
20
|
+
version: "0.1.0",
|
|
21
|
+
timestamp: new Date().toISOString(),
|
|
22
|
+
services: {
|
|
23
|
+
github: "unknown",
|
|
24
|
+
knowledge: "unknown",
|
|
25
|
+
cache: "unknown",
|
|
26
|
+
},
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
return c.json(health);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
return app;
|
|
33
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { EngineContext } from "../engine.js";
|
|
3
|
+
import { SourcePressError } from "../middleware/error-handler.js";
|
|
4
|
+
import { handleRouteError } from "../middleware/route-error-handler.js";
|
|
5
|
+
|
|
6
|
+
const MAX_BATCH_SIZE = 50;
|
|
7
|
+
|
|
8
|
+
export function importRoutes(engine: EngineContext) {
|
|
9
|
+
const app = new Hono();
|
|
10
|
+
|
|
11
|
+
app.onError(handleRouteError);
|
|
12
|
+
|
|
13
|
+
// POST /knowledge/import — scrape a single URL (synchronous)
|
|
14
|
+
app.post("/knowledge/import", async (c) => {
|
|
15
|
+
const body = await c.req.json<{ url: string }>();
|
|
16
|
+
if (!body.url) {
|
|
17
|
+
throw new SourcePressError(400, "INVALID_INPUT", "url is required");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
try {
|
|
21
|
+
const result = await engine.knowledge.importUrl(body.url);
|
|
22
|
+
return c.json(
|
|
23
|
+
{
|
|
24
|
+
imported: true,
|
|
25
|
+
path: result.path,
|
|
26
|
+
type: result.type,
|
|
27
|
+
quality: result.quality,
|
|
28
|
+
quality_score: result.quality_score,
|
|
29
|
+
entities: result.entities,
|
|
30
|
+
source_url: result.source_url,
|
|
31
|
+
},
|
|
32
|
+
201,
|
|
33
|
+
);
|
|
34
|
+
} catch (error) {
|
|
35
|
+
throw new SourcePressError(
|
|
36
|
+
422,
|
|
37
|
+
"IMPORT_FAILED",
|
|
38
|
+
error instanceof Error ? error.message : "Failed to import URL",
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
// POST /knowledge/import/batch — import multiple URLs (async job)
|
|
44
|
+
app.post("/knowledge/import/batch", async (c) => {
|
|
45
|
+
if (!engine.jobs) {
|
|
46
|
+
throw new SourcePressError(501, "JOBS_NOT_CONFIGURED", "Job system not configured");
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
const body = await c.req.json<{ urls: string[] }>();
|
|
50
|
+
if (!body.urls || !Array.isArray(body.urls) || body.urls.length === 0) {
|
|
51
|
+
throw new SourcePressError(
|
|
52
|
+
400,
|
|
53
|
+
"INVALID_INPUT",
|
|
54
|
+
"urls array is required and must not be empty",
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
if (body.urls.length > MAX_BATCH_SIZE) {
|
|
58
|
+
throw new SourcePressError(
|
|
59
|
+
400,
|
|
60
|
+
"BATCH_TOO_LARGE",
|
|
61
|
+
`Batch size ${body.urls.length} exceeds maximum of ${MAX_BATCH_SIZE}`,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
const jobId = await engine.jobs.enqueue({
|
|
66
|
+
type: "import-batch",
|
|
67
|
+
params: { urls: body.urls },
|
|
68
|
+
});
|
|
69
|
+
const jobStatus = await engine.jobs.status(jobId);
|
|
70
|
+
|
|
71
|
+
return c.json(jobStatus, 202);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
// POST /knowledge/import/sitemap — parse sitemap for interactive selection
|
|
75
|
+
app.post("/knowledge/import/sitemap", async (c) => {
|
|
76
|
+
const body = await c.req.json<{ url: string }>();
|
|
77
|
+
if (!body.url) {
|
|
78
|
+
throw new SourcePressError(400, "INVALID_INPUT", "url is required");
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
try {
|
|
82
|
+
const result = await engine.knowledge.parseSitemap(body.url);
|
|
83
|
+
return c.json(result);
|
|
84
|
+
} catch (error) {
|
|
85
|
+
throw new SourcePressError(
|
|
86
|
+
422,
|
|
87
|
+
"SITEMAP_PARSE_FAILED",
|
|
88
|
+
error instanceof Error ? error.message : "Failed to parse sitemap",
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
// POST /knowledge/import/sitemap/run — run sitemap import with filters (async job)
|
|
94
|
+
app.post("/knowledge/import/sitemap/run", async (c) => {
|
|
95
|
+
if (!engine.jobs) {
|
|
96
|
+
throw new SourcePressError(501, "JOBS_NOT_CONFIGURED", "Job system not configured");
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const body = await c.req.json<{
|
|
100
|
+
sitemap_url: string;
|
|
101
|
+
include?: string[];
|
|
102
|
+
exclude?: string[];
|
|
103
|
+
}>();
|
|
104
|
+
if (!body.sitemap_url) {
|
|
105
|
+
throw new SourcePressError(400, "INVALID_INPUT", "sitemap_url is required");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const jobId = await engine.jobs.enqueue({
|
|
109
|
+
type: "import-sitemap",
|
|
110
|
+
params: {
|
|
111
|
+
sitemap_url: body.sitemap_url,
|
|
112
|
+
include: body.include,
|
|
113
|
+
exclude: body.exclude,
|
|
114
|
+
},
|
|
115
|
+
});
|
|
116
|
+
const jobStatus = await engine.jobs.status(jobId);
|
|
117
|
+
|
|
118
|
+
return c.json(jobStatus, 202);
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
return app;
|
|
122
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
export { approvalRoutes } from "./approval.js";
|
|
2
|
+
export { healthRoutes } from "./health.js";
|
|
3
|
+
export { contentRoutes } from "./content.js";
|
|
4
|
+
export { knowledgeRoutes } from "./knowledge.js";
|
|
5
|
+
export { graphRoutes } from "./graph.js";
|
|
6
|
+
export { schemaRoutes } from "./schema.js";
|
|
7
|
+
export { intentRoutes } from "./intent.js";
|
|
8
|
+
export { evalRoutes } from "./eval.js";
|
|
9
|
+
export { mediaRoutes } from "./media.js";
|
|
10
|
+
export { jobRoutes } from "./jobs.js";
|
|
11
|
+
export { importRoutes } from "./import.js";
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import { Hono } from "hono";
|
|
2
|
+
import type { EngineContext } from "../engine.js";
|
|
3
|
+
import { SourcePressError } from "../middleware/error-handler.js";
|
|
4
|
+
import { handleRouteError } from "../middleware/route-error-handler.js";
|
|
5
|
+
|
|
6
|
+
export function intentRoutes(engine: EngineContext) {
|
|
7
|
+
const app = new Hono();
|
|
8
|
+
|
|
9
|
+
// GET /intent — list intent files
|
|
10
|
+
app.get("/intent", async (c) => {
|
|
11
|
+
const intentPath = engine.config.intent.path;
|
|
12
|
+
const tree = await engine.github.getTree();
|
|
13
|
+
const intentFiles = tree
|
|
14
|
+
.filter((entry) => entry.path.startsWith(intentPath) && entry.type === "blob")
|
|
15
|
+
.map((entry) => ({
|
|
16
|
+
path: entry.path,
|
|
17
|
+
name: entry.path.replace(intentPath, "").replace(/\.(md|yaml|json)$/, ""),
|
|
18
|
+
}));
|
|
19
|
+
|
|
20
|
+
return c.json({ items: intentFiles, total: intentFiles.length });
|
|
21
|
+
});
|
|
22
|
+
|
|
23
|
+
// GET /intent/:name — get a single intent file
|
|
24
|
+
app.get("/intent/:name", async (c) => {
|
|
25
|
+
const name = c.req.param("name");
|
|
26
|
+
const intentPath = engine.config.intent.path;
|
|
27
|
+
const filePath = `${intentPath}${name}.md`;
|
|
28
|
+
const file = await engine.github.getFile(filePath);
|
|
29
|
+
|
|
30
|
+
if (!file) {
|
|
31
|
+
throw new SourcePressError(404, "INTENT_NOT_FOUND", `Intent "${name}" not found`);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
return c.json({
|
|
35
|
+
name,
|
|
36
|
+
path: file.path,
|
|
37
|
+
content: file.content,
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
// PUT /intent/:name — update an intent file (direct commit, intent is human-owned)
|
|
42
|
+
app.put("/intent/:name", async (c) => {
|
|
43
|
+
const name = c.req.param("name");
|
|
44
|
+
const intentPath = engine.config.intent.path;
|
|
45
|
+
const filePath = `${intentPath}${name}.md`;
|
|
46
|
+
|
|
47
|
+
const body = await c.req.json<{ content: string }>();
|
|
48
|
+
|
|
49
|
+
if (!body.content) {
|
|
50
|
+
throw new SourcePressError(400, "INVALID_INPUT", "content is required");
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// Check if file exists to get SHA
|
|
54
|
+
const existing = await engine.github.getFile(filePath);
|
|
55
|
+
const existingSha = existing?.sha;
|
|
56
|
+
|
|
57
|
+
const result = await engine.github.createOrUpdateFile(
|
|
58
|
+
filePath,
|
|
59
|
+
body.content,
|
|
60
|
+
`update(intent): update ${name}`,
|
|
61
|
+
undefined,
|
|
62
|
+
existingSha,
|
|
63
|
+
);
|
|
64
|
+
|
|
65
|
+
return c.json({
|
|
66
|
+
updated: true,
|
|
67
|
+
path: filePath,
|
|
68
|
+
commit_sha: result.commit_sha,
|
|
69
|
+
});
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
// GET /intent/:name/impact — analyze which content is affected by this intent
|
|
73
|
+
app.get("/intent/:name/impact", async (c) => {
|
|
74
|
+
const name = c.req.param("name");
|
|
75
|
+
const intentPath = engine.config.intent.path;
|
|
76
|
+
const filePath = `${intentPath}${name}.md`;
|
|
77
|
+
const file = await engine.github.getFile(filePath);
|
|
78
|
+
|
|
79
|
+
if (!file) {
|
|
80
|
+
throw new SourcePressError(404, "INTENT_NOT_FOUND", `Intent "${name}" not found`);
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Collect all content
|
|
84
|
+
const collections = engine.listCollections();
|
|
85
|
+
const allContent = [];
|
|
86
|
+
for (const coll of collections) {
|
|
87
|
+
const files = await engine.listContent(coll);
|
|
88
|
+
allContent.push(...files);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// Return content count — full AI-based impact analysis requires
|
|
92
|
+
// the previous version of intent, which we defer to a future enhancement
|
|
93
|
+
return c.json({
|
|
94
|
+
intent: name,
|
|
95
|
+
content_count: allContent.length,
|
|
96
|
+
message:
|
|
97
|
+
"Full AI-based intent impact analysis requires previous intent version. Use POST /api/intent/:name/impact with previous_content field for detailed analysis.",
|
|
98
|
+
content_paths: allContent.map((f) => f.path),
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
app.onError(handleRouteError);
|
|
103
|
+
|
|
104
|
+
return app;
|
|
105
|
+
}
|