@kaddo/cli 3.72.2 → 3.74.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/dist/admin-dist/assets/index-AAaxSvT4.js +38 -0
- package/dist/admin-dist/assets/index-BPt-9--k.css +2 -0
- package/dist/admin-dist/index.html +2 -2
- package/dist/admin-server/index.js +740 -225
- package/dist/core.js +757 -11
- package/package.json +1 -1
- package/dist/admin-dist/assets/index-D4sr_XCt.css +0 -2
- package/dist/admin-dist/assets/index-DEZExhWd.js +0 -38
|
@@ -61,6 +61,13 @@ import {
|
|
|
61
61
|
getWorkItems as coreGetWorkItems,
|
|
62
62
|
getWorkItem as coreGetWorkItem,
|
|
63
63
|
WorkItemNotFoundError,
|
|
64
|
+
createWorkItem as coreCreateWorkItem,
|
|
65
|
+
updateWorkItem as coreUpdateWorkItem,
|
|
66
|
+
getWorkItemForEdit as coreGetWorkItemForEdit,
|
|
67
|
+
validateWorkItem as coreValidateWorkItem,
|
|
68
|
+
transitionWorkItem as coreTransitionWorkItem,
|
|
69
|
+
getWorkItemCaptureDefinition as coreGetCaptureDefinition,
|
|
70
|
+
WorkItemWriteError,
|
|
64
71
|
exists,
|
|
65
72
|
join,
|
|
66
73
|
readFile
|
|
@@ -114,6 +121,60 @@ function getWorkItemDetail(dir, workItemId) {
|
|
|
114
121
|
throw err;
|
|
115
122
|
}
|
|
116
123
|
}
|
|
124
|
+
function assertValidWorkItemId(workItemId) {
|
|
125
|
+
if (!workItemId || workItemId.includes("..") || workItemId.includes("/") || workItemId.includes("\\") || workItemId.startsWith(".")) {
|
|
126
|
+
throw new CoreError("INVALID_WORK_ITEM_ID", "Invalid Work Item identifier.");
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
function mapWriteError(err) {
|
|
130
|
+
if (err instanceof WorkItemWriteError) throw new CoreError(err.code, err.message);
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
function getCaptureDefinition() {
|
|
134
|
+
return coreGetCaptureDefinition();
|
|
135
|
+
}
|
|
136
|
+
function createWorkItemAdmin(dir, body) {
|
|
137
|
+
try {
|
|
138
|
+
const res = coreCreateWorkItem(dir, { intent: body.intent, type: body.type, answers: body.answers });
|
|
139
|
+
return { id: res.id, path: res.path, revision: res.revision, status: "draft" };
|
|
140
|
+
} catch (err) {
|
|
141
|
+
mapWriteError(err);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
function getWorkItemEdit(dir, workItemId) {
|
|
145
|
+
assertValidWorkItemId(workItemId);
|
|
146
|
+
try {
|
|
147
|
+
return coreGetWorkItemForEdit(dir, workItemId);
|
|
148
|
+
} catch (err) {
|
|
149
|
+
mapWriteError(err);
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
function updateWorkItemAdmin(dir, workItemId, body) {
|
|
153
|
+
assertValidWorkItemId(workItemId);
|
|
154
|
+
try {
|
|
155
|
+
const res = coreUpdateWorkItem(dir, workItemId, body.model, body.expectedRevision);
|
|
156
|
+
return { id: workItemId, path: res.path, revision: res.revision };
|
|
157
|
+
} catch (err) {
|
|
158
|
+
mapWriteError(err);
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
function validateWorkItemAdmin(dir, workItemId) {
|
|
162
|
+
assertValidWorkItemId(workItemId);
|
|
163
|
+
try {
|
|
164
|
+
return coreValidateWorkItem(dir, workItemId);
|
|
165
|
+
} catch (err) {
|
|
166
|
+
mapWriteError(err);
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
function transitionWorkItemAdmin(dir, workItemId, to, expectedRevision) {
|
|
170
|
+
assertValidWorkItemId(workItemId);
|
|
171
|
+
try {
|
|
172
|
+
const res = coreTransitionWorkItem(dir, workItemId, to, expectedRevision);
|
|
173
|
+
return { id: workItemId, path: res.path, revision: res.revision, status: res.status };
|
|
174
|
+
} catch (err) {
|
|
175
|
+
mapWriteError(err);
|
|
176
|
+
}
|
|
177
|
+
}
|
|
117
178
|
function getModules(dir) {
|
|
118
179
|
const mapped = loadMappedModules(dir);
|
|
119
180
|
return {
|
|
@@ -255,226 +316,6 @@ var CoreError = class extends Error {
|
|
|
255
316
|
code;
|
|
256
317
|
};
|
|
257
318
|
|
|
258
|
-
// src/server.ts
|
|
259
|
-
async function createAdminServer(opts) {
|
|
260
|
-
const { projectDir, storage, staticDir, host = "127.0.0.1", port = 4173 } = opts;
|
|
261
|
-
const app = Fastify({ logger: false });
|
|
262
|
-
const sessionManager = new SessionManager(storage);
|
|
263
|
-
await app.register(fastifyCookie);
|
|
264
|
-
await app.register(fastifyCors, {
|
|
265
|
-
origin: `http://${host}:${port}`,
|
|
266
|
-
credentials: true
|
|
267
|
-
});
|
|
268
|
-
if (staticDir) {
|
|
269
|
-
await app.register(fastifyStatic, {
|
|
270
|
-
root: path.resolve(staticDir),
|
|
271
|
-
prefix: "/",
|
|
272
|
-
wildcard: false
|
|
273
|
-
});
|
|
274
|
-
}
|
|
275
|
-
const sessionId = sessionManager.createSession();
|
|
276
|
-
app.addHook("onRequest", async (request, reply) => {
|
|
277
|
-
if (!request.url.startsWith("/api/")) return;
|
|
278
|
-
if (request.url.startsWith("/api/v1/admin/session")) return;
|
|
279
|
-
if (request.url.startsWith("/api/v1/admin/health")) return;
|
|
280
|
-
const cookieSession = request.cookies["kaddo-session"];
|
|
281
|
-
if (!sessionManager.validateSession(cookieSession)) {
|
|
282
|
-
reply.code(401).send({ error: { code: "SESSION_INVALID", message: "Invalid or expired session." } });
|
|
283
|
-
}
|
|
284
|
-
});
|
|
285
|
-
app.get("/api/v1/admin/health", async () => ({ status: "ok" }));
|
|
286
|
-
app.get("/api/v1/admin/session", async (_request, reply) => {
|
|
287
|
-
reply.setCookie("kaddo-session", sessionId, {
|
|
288
|
-
path: "/",
|
|
289
|
-
httpOnly: true,
|
|
290
|
-
sameSite: "strict",
|
|
291
|
-
maxAge: 86400
|
|
292
|
-
});
|
|
293
|
-
return { status: "active" };
|
|
294
|
-
});
|
|
295
|
-
const coreRoute = (handler) => {
|
|
296
|
-
return async () => {
|
|
297
|
-
try {
|
|
298
|
-
return handler(projectDir);
|
|
299
|
-
} catch (err) {
|
|
300
|
-
if (err instanceof CoreError) {
|
|
301
|
-
return { error: { code: err.code, message: err.message } };
|
|
302
|
-
}
|
|
303
|
-
throw err;
|
|
304
|
-
}
|
|
305
|
-
};
|
|
306
|
-
};
|
|
307
|
-
app.get("/api/v1/admin/overview", coreRoute(getProjectOverview));
|
|
308
|
-
app.get("/api/v1/admin/project", coreRoute(getProjectSummary));
|
|
309
|
-
app.get("/api/v1/admin/knowledge", coreRoute(getKnowledgeSummary));
|
|
310
|
-
app.get(
|
|
311
|
-
"/api/v1/admin/work-items",
|
|
312
|
-
async (request) => {
|
|
313
|
-
try {
|
|
314
|
-
const { status, module, query } = request.query;
|
|
315
|
-
return getWorkItemsList(projectDir, { status, module, query });
|
|
316
|
-
} catch (err) {
|
|
317
|
-
if (err instanceof CoreError) {
|
|
318
|
-
return { error: { code: err.code, message: err.message } };
|
|
319
|
-
}
|
|
320
|
-
throw err;
|
|
321
|
-
}
|
|
322
|
-
}
|
|
323
|
-
);
|
|
324
|
-
app.get("/api/v1/admin/work-items/:workItemId", async (request, reply) => {
|
|
325
|
-
try {
|
|
326
|
-
return getWorkItemDetail(projectDir, request.params.workItemId);
|
|
327
|
-
} catch (err) {
|
|
328
|
-
if (err instanceof CoreError) {
|
|
329
|
-
const code = err.code === "WORK_ITEM_NOT_FOUND" ? 404 : err.code === "INVALID_WORK_ITEM_ID" ? 400 : 500;
|
|
330
|
-
return reply.code(code).send({ error: { code: err.code, message: err.message } });
|
|
331
|
-
}
|
|
332
|
-
throw err;
|
|
333
|
-
}
|
|
334
|
-
});
|
|
335
|
-
app.get("/api/v1/admin/modules", coreRoute(getModules));
|
|
336
|
-
app.get("/api/v1/admin/readiness", coreRoute(getProjectReadiness));
|
|
337
|
-
app.get("/api/v1/admin/route", coreRoute(getProjectRoute));
|
|
338
|
-
app.get("/api/v1/admin/findings", coreRoute(getFindings));
|
|
339
|
-
app.get("/api/v1/admin/knowledge/inventory", coreRoute(getKnowledgeInventory));
|
|
340
|
-
app.get("/api/v1/admin/knowledge/artifact/:artifactId", async (request) => {
|
|
341
|
-
try {
|
|
342
|
-
return getKnowledgeArtifactDetail(projectDir, request.params.artifactId);
|
|
343
|
-
} catch (err) {
|
|
344
|
-
if (err instanceof CoreError) {
|
|
345
|
-
return { error: { code: err.code, message: err.message } };
|
|
346
|
-
}
|
|
347
|
-
throw err;
|
|
348
|
-
}
|
|
349
|
-
});
|
|
350
|
-
if (staticDir) {
|
|
351
|
-
app.setNotFoundHandler(async (_request, reply) => {
|
|
352
|
-
return reply.sendFile("index.html");
|
|
353
|
-
});
|
|
354
|
-
}
|
|
355
|
-
return {
|
|
356
|
-
app,
|
|
357
|
-
sessionId,
|
|
358
|
-
sessionManager,
|
|
359
|
-
start: async () => {
|
|
360
|
-
await app.listen({ host, port });
|
|
361
|
-
return `http://${host}:${port}`;
|
|
362
|
-
},
|
|
363
|
-
stop: async () => {
|
|
364
|
-
sessionManager.invalidateAll();
|
|
365
|
-
await app.close();
|
|
366
|
-
await storage.close();
|
|
367
|
-
}
|
|
368
|
-
};
|
|
369
|
-
}
|
|
370
|
-
|
|
371
|
-
// src/storage/sqlite-storage.ts
|
|
372
|
-
import { DatabaseSync } from "node:sqlite";
|
|
373
|
-
var SQLiteAdminStorage = class {
|
|
374
|
-
constructor(dbPath) {
|
|
375
|
-
this.dbPath = dbPath;
|
|
376
|
-
this.db = new DatabaseSync(dbPath);
|
|
377
|
-
this.db.exec("PRAGMA journal_mode = WAL");
|
|
378
|
-
this.sessions = {
|
|
379
|
-
create: (session) => {
|
|
380
|
-
this.db.prepare(
|
|
381
|
-
"INSERT OR REPLACE INTO admin_sessions (id, created_at, expires_at) VALUES (?, ?, ?)"
|
|
382
|
-
).run(session.id, session.createdAt, session.expiresAt);
|
|
383
|
-
},
|
|
384
|
-
findById: (id) => {
|
|
385
|
-
const row = this.db.prepare(
|
|
386
|
-
"SELECT id, created_at, expires_at FROM admin_sessions WHERE id = ?"
|
|
387
|
-
).get(id);
|
|
388
|
-
if (!row) return void 0;
|
|
389
|
-
return { id: row.id, createdAt: row.created_at, expiresAt: row.expires_at };
|
|
390
|
-
},
|
|
391
|
-
deleteById: (id) => {
|
|
392
|
-
this.db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(id);
|
|
393
|
-
},
|
|
394
|
-
deleteExpired: () => {
|
|
395
|
-
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
396
|
-
this.db.prepare("DELETE FROM admin_sessions WHERE expires_at < ?").run(now);
|
|
397
|
-
}
|
|
398
|
-
};
|
|
399
|
-
this.preferences = {
|
|
400
|
-
get: (key) => {
|
|
401
|
-
const row = this.db.prepare(
|
|
402
|
-
"SELECT value FROM admin_preferences WHERE key = ?"
|
|
403
|
-
).get(key);
|
|
404
|
-
return row?.value;
|
|
405
|
-
},
|
|
406
|
-
set: (key, value) => {
|
|
407
|
-
this.db.prepare(
|
|
408
|
-
"INSERT OR REPLACE INTO admin_preferences (key, value) VALUES (?, ?)"
|
|
409
|
-
).run(key, value);
|
|
410
|
-
},
|
|
411
|
-
delete: (key) => {
|
|
412
|
-
this.db.prepare("DELETE FROM admin_preferences WHERE key = ?").run(key);
|
|
413
|
-
},
|
|
414
|
-
all: () => {
|
|
415
|
-
return this.db.prepare("SELECT key, value FROM admin_preferences").all();
|
|
416
|
-
}
|
|
417
|
-
};
|
|
418
|
-
this.cache = {
|
|
419
|
-
get: (key) => {
|
|
420
|
-
const row = this.db.prepare(
|
|
421
|
-
"SELECT value, expires_at FROM admin_cache WHERE key = ?"
|
|
422
|
-
).get(key);
|
|
423
|
-
if (!row) return void 0;
|
|
424
|
-
if (row.expires_at && new Date(row.expires_at) < /* @__PURE__ */ new Date()) {
|
|
425
|
-
this.db.prepare("DELETE FROM admin_cache WHERE key = ?").run(key);
|
|
426
|
-
return void 0;
|
|
427
|
-
}
|
|
428
|
-
return row.value;
|
|
429
|
-
},
|
|
430
|
-
set: (key, value, ttlMs) => {
|
|
431
|
-
const expiresAt = ttlMs ? new Date(Date.now() + ttlMs).toISOString() : null;
|
|
432
|
-
this.db.prepare(
|
|
433
|
-
"INSERT OR REPLACE INTO admin_cache (key, value, expires_at) VALUES (?, ?, ?)"
|
|
434
|
-
).run(key, value, expiresAt);
|
|
435
|
-
},
|
|
436
|
-
delete: (key) => {
|
|
437
|
-
this.db.prepare("DELETE FROM admin_cache WHERE key = ?").run(key);
|
|
438
|
-
},
|
|
439
|
-
clear: () => {
|
|
440
|
-
this.db.exec("DELETE FROM admin_cache");
|
|
441
|
-
}
|
|
442
|
-
};
|
|
443
|
-
}
|
|
444
|
-
dbPath;
|
|
445
|
-
db;
|
|
446
|
-
sessions;
|
|
447
|
-
preferences;
|
|
448
|
-
cache;
|
|
449
|
-
async initialize() {
|
|
450
|
-
this.db.exec(`
|
|
451
|
-
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
452
|
-
id TEXT PRIMARY KEY,
|
|
453
|
-
created_at TEXT NOT NULL,
|
|
454
|
-
expires_at TEXT NOT NULL
|
|
455
|
-
);
|
|
456
|
-
CREATE TABLE IF NOT EXISTS admin_preferences (
|
|
457
|
-
key TEXT PRIMARY KEY,
|
|
458
|
-
value TEXT NOT NULL
|
|
459
|
-
);
|
|
460
|
-
CREATE TABLE IF NOT EXISTS admin_cache (
|
|
461
|
-
key TEXT PRIMARY KEY,
|
|
462
|
-
value TEXT NOT NULL,
|
|
463
|
-
expires_at TEXT
|
|
464
|
-
);
|
|
465
|
-
CREATE TABLE IF NOT EXISTS audit_events (
|
|
466
|
-
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
467
|
-
timestamp TEXT NOT NULL,
|
|
468
|
-
action TEXT NOT NULL,
|
|
469
|
-
detail TEXT
|
|
470
|
-
);
|
|
471
|
-
`);
|
|
472
|
-
}
|
|
473
|
-
async close() {
|
|
474
|
-
this.db.close();
|
|
475
|
-
}
|
|
476
|
-
};
|
|
477
|
-
|
|
478
319
|
// src/contracts/schemas.ts
|
|
479
320
|
import { z } from "zod";
|
|
480
321
|
var ProjectSummarySchema = z.object({
|
|
@@ -668,12 +509,676 @@ var WorkItemDetailSchema = WorkItemListItemSchema.extend({
|
|
|
668
509
|
source: z.object({ type: z.string(), id: z.string().optional(), inferred: z.boolean() }).passthrough(),
|
|
669
510
|
path: z.string()
|
|
670
511
|
});
|
|
671
|
-
var
|
|
672
|
-
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
|
|
512
|
+
var WorkItemInputSchema = z.object({
|
|
513
|
+
title: z.string(),
|
|
514
|
+
type: z.string(),
|
|
515
|
+
summary: z.string().optional(),
|
|
516
|
+
actor: z.string().optional(),
|
|
517
|
+
outcome: z.string().optional(),
|
|
518
|
+
currentBehavior: z.string().optional(),
|
|
519
|
+
targetBehavior: z.string().optional(),
|
|
520
|
+
entryPoints: z.string().optional(),
|
|
521
|
+
endToEndFlow: z.string().optional(),
|
|
522
|
+
scopeConfidence: z.object({ level: z.string(), reasons: z.array(z.string()) }).nullable(),
|
|
523
|
+
scopeUnknowns: z.array(z.string()),
|
|
524
|
+
affectedModules: z.array(z.string()),
|
|
525
|
+
moduleCoverage: z.array(z.object({ id: z.string(), status: z.string(), reason: z.string().optional() })),
|
|
526
|
+
impactAnalysis: z.array(z.object({ surface: z.string(), status: z.string(), reason: z.string().optional(), question: z.string().optional() })),
|
|
527
|
+
acceptanceCriteria: z.array(z.object({ text: z.string(), checked: z.boolean().nullable() })),
|
|
528
|
+
decisions: z.array(z.string()),
|
|
529
|
+
relatedKnowledge: z.array(z.string())
|
|
676
530
|
});
|
|
531
|
+
var WorkItemCreateSchema = z.object({
|
|
532
|
+
intent: z.string().min(1),
|
|
533
|
+
type: z.string().min(1)
|
|
534
|
+
});
|
|
535
|
+
var WorkItemUpdateSchema = z.object({
|
|
536
|
+
model: WorkItemInputSchema,
|
|
537
|
+
expectedRevision: z.string().min(1)
|
|
538
|
+
});
|
|
539
|
+
var WorkItemTransitionSchema = z.object({
|
|
540
|
+
expectedRevision: z.string().min(1)
|
|
541
|
+
});
|
|
542
|
+
var WorkItemEditModelSchema = WorkItemInputSchema.extend({
|
|
543
|
+
id: z.string(),
|
|
544
|
+
status: z.string(),
|
|
545
|
+
revision: z.string(),
|
|
546
|
+
path: z.string(),
|
|
547
|
+
editable: z.boolean(),
|
|
548
|
+
editableReason: z.string().optional()
|
|
549
|
+
});
|
|
550
|
+
var ValidationResultSchema = z.object({
|
|
551
|
+
findings: z.array(z.object({ level: z.enum(["blocking", "warning", "fyi"]), message: z.string() })),
|
|
552
|
+
canMarkReady: z.boolean()
|
|
553
|
+
});
|
|
554
|
+
var WorkItemWriteResultSchema = z.object({
|
|
555
|
+
id: z.string(),
|
|
556
|
+
path: z.string(),
|
|
557
|
+
revision: z.string(),
|
|
558
|
+
status: z.string().optional()
|
|
559
|
+
});
|
|
560
|
+
var WorkItemCreateWithAnswersSchema = z.object({
|
|
561
|
+
intent: z.string().min(1),
|
|
562
|
+
type: z.string().min(1),
|
|
563
|
+
answers: z.record(z.string(), z.string()).optional()
|
|
564
|
+
});
|
|
565
|
+
var RefinementFeedbackSchema = z.object({
|
|
566
|
+
refinementId: z.string().min(1),
|
|
567
|
+
feedback: z.string().min(1)
|
|
568
|
+
});
|
|
569
|
+
var RefinementApplySchema = z.object({
|
|
570
|
+
refinementId: z.string().min(1),
|
|
571
|
+
expectedRevision: z.string().min(1)
|
|
572
|
+
});
|
|
573
|
+
var ErrorResponseSchema = z.object({
|
|
574
|
+
error: z.object({
|
|
575
|
+
code: z.string(),
|
|
576
|
+
message: z.string()
|
|
577
|
+
})
|
|
578
|
+
});
|
|
579
|
+
|
|
580
|
+
// src/refinement/service.ts
|
|
581
|
+
import crypto from "crypto";
|
|
582
|
+
import {
|
|
583
|
+
assembleRefinementContext,
|
|
584
|
+
getWorkItemAgentAssets,
|
|
585
|
+
normalizeAndValidateProposal,
|
|
586
|
+
applyRefinement,
|
|
587
|
+
WorkItemWriteError as WorkItemWriteError2
|
|
588
|
+
} from "@kaddo/cli/core";
|
|
589
|
+
|
|
590
|
+
// src/refinement/provider.ts
|
|
591
|
+
var RefinementProviderError = class extends Error {
|
|
592
|
+
code;
|
|
593
|
+
constructor(code, message) {
|
|
594
|
+
super(message);
|
|
595
|
+
this.name = "RefinementProviderError";
|
|
596
|
+
this.code = code;
|
|
597
|
+
}
|
|
598
|
+
};
|
|
599
|
+
|
|
600
|
+
// src/refinement/service.ts
|
|
601
|
+
var MAX_REPAIR_ATTEMPTS = 2;
|
|
602
|
+
var DEFAULT_TIMEOUT_MS = 6e4;
|
|
603
|
+
var RefinementService = class {
|
|
604
|
+
constructor(provider, timeoutMs = DEFAULT_TIMEOUT_MS) {
|
|
605
|
+
this.provider = provider;
|
|
606
|
+
this.timeoutMs = timeoutMs;
|
|
607
|
+
}
|
|
608
|
+
provider;
|
|
609
|
+
timeoutMs;
|
|
610
|
+
sessions = /* @__PURE__ */ new Map();
|
|
611
|
+
view(s) {
|
|
612
|
+
return { ...s };
|
|
613
|
+
}
|
|
614
|
+
async run(dir, workItemId, feedback, previous) {
|
|
615
|
+
const context = assembleRefinementContext(dir, workItemId);
|
|
616
|
+
if (context.workItem.status !== "draft") {
|
|
617
|
+
throw new RefinementProviderError("WORK_ITEM_NOT_EDITABLE", `A ${context.workItem.status} Work Item cannot be refined. Reopen it as Draft first.`);
|
|
618
|
+
}
|
|
619
|
+
const assets = getWorkItemAgentAssets();
|
|
620
|
+
const request = { context, assets, intent: context.workItem.intent, previousProposal: previous, feedback };
|
|
621
|
+
let lastErr;
|
|
622
|
+
for (let attempt = 0; attempt <= MAX_REPAIR_ATTEMPTS; attempt++) {
|
|
623
|
+
const ac = new AbortController();
|
|
624
|
+
const timer = setTimeout(() => ac.abort(), this.timeoutMs);
|
|
625
|
+
try {
|
|
626
|
+
const result = await this.provider.refine(request, ac.signal);
|
|
627
|
+
const { validation } = normalizeAndValidateProposal(dir, workItemId, result.proposal);
|
|
628
|
+
const contextUsed = context.knowledge.map((k) => ({ id: k.id, title: k.title, layer: k.layer }));
|
|
629
|
+
return { context, proposal: result.proposal, validation, contextUsed, meta: { ...result.meta, repairAttempts: attempt } };
|
|
630
|
+
} catch (err) {
|
|
631
|
+
lastErr = err;
|
|
632
|
+
if (!(err instanceof RefinementProviderError && err.code === "INVALID_RESPONSE")) break;
|
|
633
|
+
} finally {
|
|
634
|
+
clearTimeout(timer);
|
|
635
|
+
}
|
|
636
|
+
}
|
|
637
|
+
throw lastErr instanceof Error ? lastErr : new RefinementProviderError("PROVIDER_ERROR", "Refinement failed.");
|
|
638
|
+
}
|
|
639
|
+
async start(dir, workItemId) {
|
|
640
|
+
const { context, proposal, validation, contextUsed, meta } = await this.run(dir, workItemId);
|
|
641
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
642
|
+
const session = {
|
|
643
|
+
refinementId: `ref_${crypto.randomBytes(8).toString("hex")}`,
|
|
644
|
+
workItemId,
|
|
645
|
+
sourceRevision: context.revision,
|
|
646
|
+
status: "ready-for-review",
|
|
647
|
+
intent: context.workItem.intent,
|
|
648
|
+
proposal,
|
|
649
|
+
validation,
|
|
650
|
+
contextUsed,
|
|
651
|
+
meta,
|
|
652
|
+
createdAt: now,
|
|
653
|
+
updatedAt: now
|
|
654
|
+
};
|
|
655
|
+
this.sessions.set(session.refinementId, session);
|
|
656
|
+
return this.view(session);
|
|
657
|
+
}
|
|
658
|
+
async feedback(dir, workItemId, refinementId, feedback) {
|
|
659
|
+
const session = this.get(refinementId, workItemId);
|
|
660
|
+
const { context, proposal, validation, contextUsed, meta } = await this.run(dir, workItemId, feedback, session.proposal);
|
|
661
|
+
session.sourceRevision = context.revision;
|
|
662
|
+
session.status = "ready-for-review";
|
|
663
|
+
session.proposal = proposal;
|
|
664
|
+
session.validation = validation;
|
|
665
|
+
session.contextUsed = contextUsed;
|
|
666
|
+
session.meta = meta;
|
|
667
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
668
|
+
return this.view(session);
|
|
669
|
+
}
|
|
670
|
+
apply(dir, workItemId, refinementId, expectedRevision) {
|
|
671
|
+
const session = this.get(refinementId, workItemId);
|
|
672
|
+
const current = assembleRefinementContext(dir, workItemId);
|
|
673
|
+
if (current.revision !== session.sourceRevision || expectedRevision !== session.sourceRevision) {
|
|
674
|
+
session.status = "stale";
|
|
675
|
+
throw new RefinementProviderError("WORK_ITEM_CONFLICT", "This Work Item changed while the refinement was running. The proposal has not been applied.");
|
|
676
|
+
}
|
|
677
|
+
let res;
|
|
678
|
+
try {
|
|
679
|
+
res = applyRefinement(dir, workItemId, session.proposal, session.sourceRevision);
|
|
680
|
+
} catch (err) {
|
|
681
|
+
if (err instanceof WorkItemWriteError2) throw new RefinementProviderError(err.code, err.message);
|
|
682
|
+
throw err;
|
|
683
|
+
}
|
|
684
|
+
session.status = "applied";
|
|
685
|
+
session.updatedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
686
|
+
return { id: workItemId, path: res.path, revision: res.revision, status: "draft" };
|
|
687
|
+
}
|
|
688
|
+
get(refinementId, workItemId) {
|
|
689
|
+
const s = this.sessions.get(refinementId);
|
|
690
|
+
if (!s || s.workItemId !== workItemId) throw new RefinementProviderError("REFINEMENT_NOT_FOUND", "Refinement session not found.");
|
|
691
|
+
return s;
|
|
692
|
+
}
|
|
693
|
+
};
|
|
694
|
+
|
|
695
|
+
// src/refinement/heuristic-provider.ts
|
|
696
|
+
function firstSentence(s) {
|
|
697
|
+
const m = s.trim().match(/^(.*?[.!?])(\s|$)/);
|
|
698
|
+
return (m ? m[1] : s.trim()).trim();
|
|
699
|
+
}
|
|
700
|
+
function mentionsNegation(feedback, module) {
|
|
701
|
+
const re = new RegExp(`(not|no)\\b[^.]*\\b${module}\\b|\\b${module}\\b[^.]*(not affected|no afecta|reviewed)`, "i");
|
|
702
|
+
return re.test(feedback);
|
|
703
|
+
}
|
|
704
|
+
var HeuristicRefinementProvider = class {
|
|
705
|
+
name = "heuristic";
|
|
706
|
+
async refine(request) {
|
|
707
|
+
const start = Date.now();
|
|
708
|
+
const { context, intent, previousProposal, feedback } = request;
|
|
709
|
+
const modules = context.modules;
|
|
710
|
+
const affected = new Set(previousProposal?.affectedModules ?? ["core"]);
|
|
711
|
+
if (feedback) {
|
|
712
|
+
for (const m of modules) {
|
|
713
|
+
if (new RegExp(`\\b${m}\\b`, "i").test(feedback)) {
|
|
714
|
+
if (mentionsNegation(feedback, m)) affected.delete(m);
|
|
715
|
+
else affected.add(m);
|
|
716
|
+
}
|
|
717
|
+
}
|
|
718
|
+
}
|
|
719
|
+
const affectedModules = modules.filter((m) => affected.has(m));
|
|
720
|
+
const moduleCoverage = modules.map(
|
|
721
|
+
(m) => affected.has(m) ? { id: m, status: "affected", reason: m === "core" ? "Backend behavior changes." : "User-facing change identified." } : { id: m, status: "unknown" }
|
|
722
|
+
);
|
|
723
|
+
const frontendAffected = affected.has("frontend");
|
|
724
|
+
const impactAnalysis = [
|
|
725
|
+
{ surface: "backend", status: "affected" },
|
|
726
|
+
{ surface: "frontend", status: frontendAffected ? "affected" : "unknown", ...frontendAffected ? {} : { question: "Is a user-facing surface involved?" } },
|
|
727
|
+
{ surface: "database", status: "reviewed-not-affected" },
|
|
728
|
+
{ surface: "feature-flags", status: "unknown", question: "Is this behavior controlled by a feature flag?" }
|
|
729
|
+
];
|
|
730
|
+
const summary = firstSentence(intent);
|
|
731
|
+
const proposal = {
|
|
732
|
+
outcome: {
|
|
733
|
+
actor: "User",
|
|
734
|
+
observableOutcome: summary,
|
|
735
|
+
currentBehavior: `Today: ${summary.toLowerCase()} is not yet supported as described.`,
|
|
736
|
+
targetBehavior: summary
|
|
737
|
+
},
|
|
738
|
+
journey: {
|
|
739
|
+
entryPoints: [frontendAffected ? "Public entry point" : "Application entry point"],
|
|
740
|
+
flow: ["Entry point", "Application logic", "Persistence", "Result"]
|
|
741
|
+
},
|
|
742
|
+
affectedModules,
|
|
743
|
+
moduleCoverage,
|
|
744
|
+
impactAnalysis,
|
|
745
|
+
scopeConfidence: {
|
|
746
|
+
level: "medium",
|
|
747
|
+
reasons: ["Primary behavior identified from the intent.", "Feature flag ownership not yet confirmed."]
|
|
748
|
+
},
|
|
749
|
+
scopeUnknowns: ["Is this behavior controlled by a feature flag?"],
|
|
750
|
+
acceptanceCriteria: [
|
|
751
|
+
`${summary}`,
|
|
752
|
+
"The change is covered by the affected modules above."
|
|
753
|
+
],
|
|
754
|
+
linkedDecisions: [],
|
|
755
|
+
relatedKnowledge: []
|
|
756
|
+
};
|
|
757
|
+
return { proposal, meta: { provider: this.name, durationMs: Date.now() - start, repairAttempts: 0 } };
|
|
758
|
+
}
|
|
759
|
+
};
|
|
760
|
+
|
|
761
|
+
// src/refinement/anthropic-provider.ts
|
|
762
|
+
var API_URL = "https://api.anthropic.com/v1/messages";
|
|
763
|
+
var SCHEMA_HINT = `Return ONLY a JSON object (no prose, no code fences) with this shape \u2014 omit fields you cannot determine:
|
|
764
|
+
{
|
|
765
|
+
"title"?: string,
|
|
766
|
+
"outcome"?: { "actor"?: string, "observableOutcome"?: string, "currentBehavior"?: string, "targetBehavior"?: string },
|
|
767
|
+
"journey"?: { "entryPoints"?: string[], "flow"?: string[] },
|
|
768
|
+
"affectedModules"?: string[],
|
|
769
|
+
"moduleCoverage"?: [{ "id": string, "status": "affected"|"reviewed-not-affected"|"unknown"|"not-applicable", "reason"?: string }],
|
|
770
|
+
"impactAnalysis"?: [{ "surface": string, "status": "affected"|"reviewed-not-affected"|"unknown"|"not-applicable", "reason"?: string, "question"?: string }],
|
|
771
|
+
"scopeConfidence"?: { "level": "high"|"medium"|"low", "reasons"?: string[] },
|
|
772
|
+
"scopeUnknowns"?: string[],
|
|
773
|
+
"acceptanceCriteria"?: string[],
|
|
774
|
+
"linkedDecisions"?: string[],
|
|
775
|
+
"relatedKnowledge"?: string[]
|
|
776
|
+
}
|
|
777
|
+
Only reference module ids, decision ids and knowledge ids that appear in the provided context. Prefer "unknown" over inventing facts.`;
|
|
778
|
+
function extractJson(text) {
|
|
779
|
+
const start = text.indexOf("{");
|
|
780
|
+
const end = text.lastIndexOf("}");
|
|
781
|
+
if (start < 0 || end <= start) throw new RefinementProviderError("INVALID_RESPONSE", "The model did not return a JSON proposal.");
|
|
782
|
+
try {
|
|
783
|
+
return JSON.parse(text.slice(start, end + 1));
|
|
784
|
+
} catch {
|
|
785
|
+
throw new RefinementProviderError("INVALID_RESPONSE", "The model returned a proposal that could not be parsed.");
|
|
786
|
+
}
|
|
787
|
+
}
|
|
788
|
+
var AnthropicRefinementProvider = class {
|
|
789
|
+
name = "anthropic";
|
|
790
|
+
apiKey;
|
|
791
|
+
model;
|
|
792
|
+
constructor(apiKey, model) {
|
|
793
|
+
this.apiKey = apiKey;
|
|
794
|
+
this.model = model;
|
|
795
|
+
}
|
|
796
|
+
async refine(request, signal) {
|
|
797
|
+
const start = Date.now();
|
|
798
|
+
const { context, assets, intent, previousProposal, feedback } = request;
|
|
799
|
+
const system = [
|
|
800
|
+
assets.agentPrompt,
|
|
801
|
+
assets.skill ?? "",
|
|
802
|
+
"# Output format",
|
|
803
|
+
SCHEMA_HINT
|
|
804
|
+
].filter(Boolean).join("\n\n");
|
|
805
|
+
const userParts = [
|
|
806
|
+
`# Work Item intent
|
|
807
|
+
${intent}`,
|
|
808
|
+
`# Project
|
|
809
|
+
${JSON.stringify(context.project)}`,
|
|
810
|
+
`# Registered modules
|
|
811
|
+
${context.modules.join(", ")}`,
|
|
812
|
+
`# Known decisions
|
|
813
|
+
${context.decisions.map((d) => `${d.id} \u2014 ${d.title}`).join("\n") || "(none)"}`,
|
|
814
|
+
`# Knowledge
|
|
815
|
+
${context.knowledge.map((k) => `${k.id} \u2014 ${k.title} (${k.layer})`).join("\n") || "(none)"}`,
|
|
816
|
+
`# Current Work Item model
|
|
817
|
+
${JSON.stringify(context.workItem.current)}`
|
|
818
|
+
];
|
|
819
|
+
if (previousProposal) userParts.push(`# Previous proposal
|
|
820
|
+
${JSON.stringify(previousProposal)}`);
|
|
821
|
+
if (feedback) userParts.push(`# Human feedback (augments the original intent, does not replace it)
|
|
822
|
+
${feedback}`);
|
|
823
|
+
let res;
|
|
824
|
+
try {
|
|
825
|
+
res = await fetch(API_URL, {
|
|
826
|
+
method: "POST",
|
|
827
|
+
signal,
|
|
828
|
+
headers: {
|
|
829
|
+
"content-type": "application/json",
|
|
830
|
+
"x-api-key": this.apiKey,
|
|
831
|
+
"anthropic-version": "2023-06-01"
|
|
832
|
+
},
|
|
833
|
+
body: JSON.stringify({
|
|
834
|
+
model: this.model,
|
|
835
|
+
max_tokens: 2048,
|
|
836
|
+
system,
|
|
837
|
+
messages: [{ role: "user", content: userParts.join("\n\n") }]
|
|
838
|
+
})
|
|
839
|
+
});
|
|
840
|
+
} catch (err) {
|
|
841
|
+
if (err.name === "AbortError") throw new RefinementProviderError("TIMEOUT", "The refinement timed out.");
|
|
842
|
+
throw new RefinementProviderError("PROVIDER_ERROR", "The refinement provider could not be reached.");
|
|
843
|
+
}
|
|
844
|
+
if (!res.ok) {
|
|
845
|
+
throw new RefinementProviderError("PROVIDER_ERROR", `The refinement provider returned an error (${res.status}).`);
|
|
846
|
+
}
|
|
847
|
+
const body = await res.json();
|
|
848
|
+
const text = (body.content ?? []).filter((c) => c.type === "text").map((c) => c.text ?? "").join("");
|
|
849
|
+
const proposal = extractJson(text);
|
|
850
|
+
return {
|
|
851
|
+
proposal,
|
|
852
|
+
meta: {
|
|
853
|
+
provider: this.name,
|
|
854
|
+
model: this.model,
|
|
855
|
+
durationMs: Date.now() - start,
|
|
856
|
+
inputTokens: body.usage?.input_tokens,
|
|
857
|
+
outputTokens: body.usage?.output_tokens
|
|
858
|
+
}
|
|
859
|
+
};
|
|
860
|
+
}
|
|
861
|
+
};
|
|
862
|
+
|
|
863
|
+
// src/refinement/index.ts
|
|
864
|
+
function createRefinementService() {
|
|
865
|
+
const key = process.env.ANTHROPIC_API_KEY;
|
|
866
|
+
const model = process.env.KADDO_REFINEMENT_MODEL || "claude-3-5-sonnet-latest";
|
|
867
|
+
const provider = key ? new AnthropicRefinementProvider(key, model) : new HeuristicRefinementProvider();
|
|
868
|
+
return new RefinementService(provider);
|
|
869
|
+
}
|
|
870
|
+
|
|
871
|
+
// src/server.ts
|
|
872
|
+
var WRITE_METHODS = /* @__PURE__ */ new Set(["POST", "PUT", "PATCH", "DELETE"]);
|
|
873
|
+
function statusForCode(code) {
|
|
874
|
+
switch (code) {
|
|
875
|
+
case "WORK_ITEM_NOT_FOUND":
|
|
876
|
+
case "REFINEMENT_NOT_FOUND":
|
|
877
|
+
return 404;
|
|
878
|
+
case "WORK_ITEM_CONFLICT":
|
|
879
|
+
case "WORK_ITEM_NOT_EDITABLE":
|
|
880
|
+
return 409;
|
|
881
|
+
case "INVALID_INPUT":
|
|
882
|
+
case "INVALID_WORK_ITEM_ID":
|
|
883
|
+
case "INVALID_TRANSITION":
|
|
884
|
+
return 400;
|
|
885
|
+
case "TIMEOUT":
|
|
886
|
+
return 504;
|
|
887
|
+
case "PROVIDER_ERROR":
|
|
888
|
+
case "INVALID_RESPONSE":
|
|
889
|
+
return 502;
|
|
890
|
+
default:
|
|
891
|
+
return 500;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
async function createAdminServer(opts) {
|
|
895
|
+
const { projectDir, storage, staticDir, host = "127.0.0.1", port = 4173 } = opts;
|
|
896
|
+
const app = Fastify({ logger: false });
|
|
897
|
+
const sessionManager = new SessionManager(storage);
|
|
898
|
+
const refinement = createRefinementService();
|
|
899
|
+
await app.register(fastifyCookie);
|
|
900
|
+
await app.register(fastifyCors, {
|
|
901
|
+
origin: `http://${host}:${port}`,
|
|
902
|
+
credentials: true
|
|
903
|
+
});
|
|
904
|
+
if (staticDir) {
|
|
905
|
+
await app.register(fastifyStatic, {
|
|
906
|
+
root: path.resolve(staticDir),
|
|
907
|
+
prefix: "/",
|
|
908
|
+
wildcard: false
|
|
909
|
+
});
|
|
910
|
+
}
|
|
911
|
+
const sessionId = sessionManager.createSession();
|
|
912
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
913
|
+
if (!request.url.startsWith("/api/")) return;
|
|
914
|
+
if (request.url.startsWith("/api/v1/admin/session")) return;
|
|
915
|
+
if (request.url.startsWith("/api/v1/admin/health")) return;
|
|
916
|
+
const cookieSession = request.cookies["kaddo-session"];
|
|
917
|
+
if (!sessionManager.validateSession(cookieSession)) {
|
|
918
|
+
reply.code(401).send({ error: { code: "SESSION_INVALID", message: "Invalid or expired session." } });
|
|
919
|
+
}
|
|
920
|
+
});
|
|
921
|
+
const allowedOrigin = `http://${host}:${port}`;
|
|
922
|
+
app.addHook("onRequest", async (request, reply) => {
|
|
923
|
+
if (!request.url.startsWith("/api/")) return;
|
|
924
|
+
if (!WRITE_METHODS.has(request.method)) return;
|
|
925
|
+
const origin = request.headers.origin;
|
|
926
|
+
if (!origin || origin !== allowedOrigin) {
|
|
927
|
+
reply.code(403).send({ error: { code: "FORBIDDEN_ORIGIN", message: "Cross-origin write requests are not allowed." } });
|
|
928
|
+
}
|
|
929
|
+
});
|
|
930
|
+
app.get("/api/v1/admin/health", async () => ({ status: "ok" }));
|
|
931
|
+
app.get("/api/v1/admin/session", async (_request, reply) => {
|
|
932
|
+
reply.setCookie("kaddo-session", sessionId, {
|
|
933
|
+
path: "/",
|
|
934
|
+
httpOnly: true,
|
|
935
|
+
sameSite: "strict",
|
|
936
|
+
maxAge: 86400
|
|
937
|
+
});
|
|
938
|
+
return { status: "active" };
|
|
939
|
+
});
|
|
940
|
+
const coreRoute = (handler) => {
|
|
941
|
+
return async () => {
|
|
942
|
+
try {
|
|
943
|
+
return handler(projectDir);
|
|
944
|
+
} catch (err) {
|
|
945
|
+
if (err instanceof CoreError) {
|
|
946
|
+
return { error: { code: err.code, message: err.message } };
|
|
947
|
+
}
|
|
948
|
+
throw err;
|
|
949
|
+
}
|
|
950
|
+
};
|
|
951
|
+
};
|
|
952
|
+
app.get("/api/v1/admin/overview", coreRoute(getProjectOverview));
|
|
953
|
+
app.get("/api/v1/admin/project", coreRoute(getProjectSummary));
|
|
954
|
+
app.get("/api/v1/admin/knowledge", coreRoute(getKnowledgeSummary));
|
|
955
|
+
app.get(
|
|
956
|
+
"/api/v1/admin/work-items",
|
|
957
|
+
async (request) => {
|
|
958
|
+
try {
|
|
959
|
+
const { status, module, query } = request.query;
|
|
960
|
+
return getWorkItemsList(projectDir, { status, module, query });
|
|
961
|
+
} catch (err) {
|
|
962
|
+
if (err instanceof CoreError) {
|
|
963
|
+
return { error: { code: err.code, message: err.message } };
|
|
964
|
+
}
|
|
965
|
+
throw err;
|
|
966
|
+
}
|
|
967
|
+
}
|
|
968
|
+
);
|
|
969
|
+
const writeHandler = (reply, fn) => {
|
|
970
|
+
try {
|
|
971
|
+
return fn();
|
|
972
|
+
} catch (err) {
|
|
973
|
+
if (err instanceof CoreError) {
|
|
974
|
+
return reply.code(statusForCode(err.code)).send({ error: { code: err.code, message: err.message } });
|
|
975
|
+
}
|
|
976
|
+
throw err;
|
|
977
|
+
}
|
|
978
|
+
};
|
|
979
|
+
app.get("/api/v1/admin/work-items-capture", coreRoute(() => getCaptureDefinition()));
|
|
980
|
+
app.post("/api/v1/admin/work-items", async (request, reply) => {
|
|
981
|
+
const parsed = WorkItemCreateWithAnswersSchema.safeParse(request.body);
|
|
982
|
+
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "Intent and type are required." } });
|
|
983
|
+
return writeHandler(reply, () => createWorkItemAdmin(projectDir, parsed.data));
|
|
984
|
+
});
|
|
985
|
+
app.get("/api/v1/admin/work-items/:workItemId/edit", async (request, reply) => {
|
|
986
|
+
return writeHandler(reply, () => getWorkItemEdit(projectDir, request.params.workItemId));
|
|
987
|
+
});
|
|
988
|
+
app.put("/api/v1/admin/work-items/:workItemId", async (request, reply) => {
|
|
989
|
+
const parsed = WorkItemUpdateSchema.safeParse(request.body);
|
|
990
|
+
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "A Work Item model and expectedRevision are required." } });
|
|
991
|
+
return writeHandler(reply, () => updateWorkItemAdmin(projectDir, request.params.workItemId, parsed.data));
|
|
992
|
+
});
|
|
993
|
+
app.post("/api/v1/admin/work-items/:workItemId/validate", async (request, reply) => {
|
|
994
|
+
return writeHandler(reply, () => validateWorkItemAdmin(projectDir, request.params.workItemId));
|
|
995
|
+
});
|
|
996
|
+
app.post("/api/v1/admin/work-items/:workItemId/transitions/ready", async (request, reply) => {
|
|
997
|
+
const parsed = WorkItemTransitionSchema.safeParse(request.body);
|
|
998
|
+
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "expectedRevision is required." } });
|
|
999
|
+
return writeHandler(reply, () => transitionWorkItemAdmin(projectDir, request.params.workItemId, "ready", parsed.data.expectedRevision));
|
|
1000
|
+
});
|
|
1001
|
+
app.post("/api/v1/admin/work-items/:workItemId/transitions/draft", async (request, reply) => {
|
|
1002
|
+
const parsed = WorkItemTransitionSchema.safeParse(request.body);
|
|
1003
|
+
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "expectedRevision is required." } });
|
|
1004
|
+
return writeHandler(reply, () => transitionWorkItemAdmin(projectDir, request.params.workItemId, "draft", parsed.data.expectedRevision));
|
|
1005
|
+
});
|
|
1006
|
+
const refineHandler = async (reply, fn) => {
|
|
1007
|
+
try {
|
|
1008
|
+
return await fn();
|
|
1009
|
+
} catch (err) {
|
|
1010
|
+
if (err instanceof RefinementProviderError || err instanceof CoreError) {
|
|
1011
|
+
return reply.code(statusForCode(err.code)).send({ error: { code: err.code, message: err.message } });
|
|
1012
|
+
}
|
|
1013
|
+
throw err;
|
|
1014
|
+
}
|
|
1015
|
+
};
|
|
1016
|
+
app.post("/api/v1/admin/work-items/:workItemId/refinement", async (request, reply) => {
|
|
1017
|
+
return refineHandler(reply, () => refinement.start(projectDir, request.params.workItemId));
|
|
1018
|
+
});
|
|
1019
|
+
app.post("/api/v1/admin/work-items/:workItemId/refinement/feedback", async (request, reply) => {
|
|
1020
|
+
const parsed = RefinementFeedbackSchema.safeParse(request.body);
|
|
1021
|
+
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "refinementId and feedback are required." } });
|
|
1022
|
+
return refineHandler(reply, () => refinement.feedback(projectDir, request.params.workItemId, parsed.data.refinementId, parsed.data.feedback));
|
|
1023
|
+
});
|
|
1024
|
+
app.post("/api/v1/admin/work-items/:workItemId/refinement/apply", async (request, reply) => {
|
|
1025
|
+
const parsed = RefinementApplySchema.safeParse(request.body);
|
|
1026
|
+
if (!parsed.success) return reply.code(400).send({ error: { code: "INVALID_INPUT", message: "refinementId and expectedRevision are required." } });
|
|
1027
|
+
return refineHandler(reply, () => refinement.apply(projectDir, request.params.workItemId, parsed.data.refinementId, parsed.data.expectedRevision));
|
|
1028
|
+
});
|
|
1029
|
+
app.get("/api/v1/admin/work-items/:workItemId", async (request, reply) => {
|
|
1030
|
+
try {
|
|
1031
|
+
return getWorkItemDetail(projectDir, request.params.workItemId);
|
|
1032
|
+
} catch (err) {
|
|
1033
|
+
if (err instanceof CoreError) {
|
|
1034
|
+
const code = err.code === "WORK_ITEM_NOT_FOUND" ? 404 : err.code === "INVALID_WORK_ITEM_ID" ? 400 : 500;
|
|
1035
|
+
return reply.code(code).send({ error: { code: err.code, message: err.message } });
|
|
1036
|
+
}
|
|
1037
|
+
throw err;
|
|
1038
|
+
}
|
|
1039
|
+
});
|
|
1040
|
+
app.get("/api/v1/admin/modules", coreRoute(getModules));
|
|
1041
|
+
app.get("/api/v1/admin/readiness", coreRoute(getProjectReadiness));
|
|
1042
|
+
app.get("/api/v1/admin/route", coreRoute(getProjectRoute));
|
|
1043
|
+
app.get("/api/v1/admin/findings", coreRoute(getFindings));
|
|
1044
|
+
app.get("/api/v1/admin/knowledge/inventory", coreRoute(getKnowledgeInventory));
|
|
1045
|
+
app.get("/api/v1/admin/knowledge/artifact/:artifactId", async (request) => {
|
|
1046
|
+
try {
|
|
1047
|
+
return getKnowledgeArtifactDetail(projectDir, request.params.artifactId);
|
|
1048
|
+
} catch (err) {
|
|
1049
|
+
if (err instanceof CoreError) {
|
|
1050
|
+
return { error: { code: err.code, message: err.message } };
|
|
1051
|
+
}
|
|
1052
|
+
throw err;
|
|
1053
|
+
}
|
|
1054
|
+
});
|
|
1055
|
+
if (staticDir) {
|
|
1056
|
+
app.setNotFoundHandler(async (_request, reply) => {
|
|
1057
|
+
return reply.sendFile("index.html");
|
|
1058
|
+
});
|
|
1059
|
+
}
|
|
1060
|
+
return {
|
|
1061
|
+
app,
|
|
1062
|
+
sessionId,
|
|
1063
|
+
sessionManager,
|
|
1064
|
+
start: async () => {
|
|
1065
|
+
await app.listen({ host, port });
|
|
1066
|
+
return `http://${host}:${port}`;
|
|
1067
|
+
},
|
|
1068
|
+
stop: async () => {
|
|
1069
|
+
sessionManager.invalidateAll();
|
|
1070
|
+
await app.close();
|
|
1071
|
+
await storage.close();
|
|
1072
|
+
}
|
|
1073
|
+
};
|
|
1074
|
+
}
|
|
1075
|
+
|
|
1076
|
+
// src/storage/sqlite-storage.ts
|
|
1077
|
+
import { DatabaseSync } from "node:sqlite";
|
|
1078
|
+
var SQLiteAdminStorage = class {
|
|
1079
|
+
constructor(dbPath) {
|
|
1080
|
+
this.dbPath = dbPath;
|
|
1081
|
+
this.db = new DatabaseSync(dbPath);
|
|
1082
|
+
this.db.exec("PRAGMA journal_mode = WAL");
|
|
1083
|
+
this.sessions = {
|
|
1084
|
+
create: (session) => {
|
|
1085
|
+
this.db.prepare(
|
|
1086
|
+
"INSERT OR REPLACE INTO admin_sessions (id, created_at, expires_at) VALUES (?, ?, ?)"
|
|
1087
|
+
).run(session.id, session.createdAt, session.expiresAt);
|
|
1088
|
+
},
|
|
1089
|
+
findById: (id) => {
|
|
1090
|
+
const row = this.db.prepare(
|
|
1091
|
+
"SELECT id, created_at, expires_at FROM admin_sessions WHERE id = ?"
|
|
1092
|
+
).get(id);
|
|
1093
|
+
if (!row) return void 0;
|
|
1094
|
+
return { id: row.id, createdAt: row.created_at, expiresAt: row.expires_at };
|
|
1095
|
+
},
|
|
1096
|
+
deleteById: (id) => {
|
|
1097
|
+
this.db.prepare("DELETE FROM admin_sessions WHERE id = ?").run(id);
|
|
1098
|
+
},
|
|
1099
|
+
deleteExpired: () => {
|
|
1100
|
+
const now = (/* @__PURE__ */ new Date()).toISOString();
|
|
1101
|
+
this.db.prepare("DELETE FROM admin_sessions WHERE expires_at < ?").run(now);
|
|
1102
|
+
}
|
|
1103
|
+
};
|
|
1104
|
+
this.preferences = {
|
|
1105
|
+
get: (key) => {
|
|
1106
|
+
const row = this.db.prepare(
|
|
1107
|
+
"SELECT value FROM admin_preferences WHERE key = ?"
|
|
1108
|
+
).get(key);
|
|
1109
|
+
return row?.value;
|
|
1110
|
+
},
|
|
1111
|
+
set: (key, value) => {
|
|
1112
|
+
this.db.prepare(
|
|
1113
|
+
"INSERT OR REPLACE INTO admin_preferences (key, value) VALUES (?, ?)"
|
|
1114
|
+
).run(key, value);
|
|
1115
|
+
},
|
|
1116
|
+
delete: (key) => {
|
|
1117
|
+
this.db.prepare("DELETE FROM admin_preferences WHERE key = ?").run(key);
|
|
1118
|
+
},
|
|
1119
|
+
all: () => {
|
|
1120
|
+
return this.db.prepare("SELECT key, value FROM admin_preferences").all();
|
|
1121
|
+
}
|
|
1122
|
+
};
|
|
1123
|
+
this.cache = {
|
|
1124
|
+
get: (key) => {
|
|
1125
|
+
const row = this.db.prepare(
|
|
1126
|
+
"SELECT value, expires_at FROM admin_cache WHERE key = ?"
|
|
1127
|
+
).get(key);
|
|
1128
|
+
if (!row) return void 0;
|
|
1129
|
+
if (row.expires_at && new Date(row.expires_at) < /* @__PURE__ */ new Date()) {
|
|
1130
|
+
this.db.prepare("DELETE FROM admin_cache WHERE key = ?").run(key);
|
|
1131
|
+
return void 0;
|
|
1132
|
+
}
|
|
1133
|
+
return row.value;
|
|
1134
|
+
},
|
|
1135
|
+
set: (key, value, ttlMs) => {
|
|
1136
|
+
const expiresAt = ttlMs ? new Date(Date.now() + ttlMs).toISOString() : null;
|
|
1137
|
+
this.db.prepare(
|
|
1138
|
+
"INSERT OR REPLACE INTO admin_cache (key, value, expires_at) VALUES (?, ?, ?)"
|
|
1139
|
+
).run(key, value, expiresAt);
|
|
1140
|
+
},
|
|
1141
|
+
delete: (key) => {
|
|
1142
|
+
this.db.prepare("DELETE FROM admin_cache WHERE key = ?").run(key);
|
|
1143
|
+
},
|
|
1144
|
+
clear: () => {
|
|
1145
|
+
this.db.exec("DELETE FROM admin_cache");
|
|
1146
|
+
}
|
|
1147
|
+
};
|
|
1148
|
+
}
|
|
1149
|
+
dbPath;
|
|
1150
|
+
db;
|
|
1151
|
+
sessions;
|
|
1152
|
+
preferences;
|
|
1153
|
+
cache;
|
|
1154
|
+
async initialize() {
|
|
1155
|
+
this.db.exec(`
|
|
1156
|
+
CREATE TABLE IF NOT EXISTS admin_sessions (
|
|
1157
|
+
id TEXT PRIMARY KEY,
|
|
1158
|
+
created_at TEXT NOT NULL,
|
|
1159
|
+
expires_at TEXT NOT NULL
|
|
1160
|
+
);
|
|
1161
|
+
CREATE TABLE IF NOT EXISTS admin_preferences (
|
|
1162
|
+
key TEXT PRIMARY KEY,
|
|
1163
|
+
value TEXT NOT NULL
|
|
1164
|
+
);
|
|
1165
|
+
CREATE TABLE IF NOT EXISTS admin_cache (
|
|
1166
|
+
key TEXT PRIMARY KEY,
|
|
1167
|
+
value TEXT NOT NULL,
|
|
1168
|
+
expires_at TEXT
|
|
1169
|
+
);
|
|
1170
|
+
CREATE TABLE IF NOT EXISTS audit_events (
|
|
1171
|
+
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
|
1172
|
+
timestamp TEXT NOT NULL,
|
|
1173
|
+
action TEXT NOT NULL,
|
|
1174
|
+
detail TEXT
|
|
1175
|
+
);
|
|
1176
|
+
`);
|
|
1177
|
+
}
|
|
1178
|
+
async close() {
|
|
1179
|
+
this.db.close();
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
677
1182
|
export {
|
|
678
1183
|
ErrorResponseSchema,
|
|
679
1184
|
FindingsSummarySchema,
|
|
@@ -687,12 +1192,22 @@ export {
|
|
|
687
1192
|
ProjectReadinessSchema,
|
|
688
1193
|
ProjectRouteSchema,
|
|
689
1194
|
ProjectSummarySchema,
|
|
1195
|
+
RefinementApplySchema,
|
|
1196
|
+
RefinementFeedbackSchema,
|
|
690
1197
|
RouteStepSchema,
|
|
691
1198
|
SQLiteAdminStorage,
|
|
692
1199
|
SessionManager,
|
|
1200
|
+
ValidationResultSchema,
|
|
1201
|
+
WorkItemCreateSchema,
|
|
1202
|
+
WorkItemCreateWithAnswersSchema,
|
|
693
1203
|
WorkItemDetailSchema,
|
|
1204
|
+
WorkItemEditModelSchema,
|
|
1205
|
+
WorkItemInputSchema,
|
|
694
1206
|
WorkItemListItemSchema,
|
|
695
1207
|
WorkItemSummarySchema,
|
|
1208
|
+
WorkItemTransitionSchema,
|
|
1209
|
+
WorkItemUpdateSchema,
|
|
1210
|
+
WorkItemWriteResultSchema,
|
|
696
1211
|
WorkItemsListSchema,
|
|
697
1212
|
WorkItemsSummaryStatsSchema,
|
|
698
1213
|
createAdminServer
|