agentwrangler 0.1.1-next.bca85ea → 0.1.1-next.bdce596
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/README.md +5 -3
- package/dist/daemon/http.js +175 -34
- package/dist/daemon/index.js +44 -81
- package/dist/daemon/lifecycle.js +122 -0
- package/dist/daemon/router.js +339 -9
- package/dist/db/migrations/019_work_records.sql +291 -0
- package/dist/db/migrations/020_local_command_retention.sql +50 -0
- package/dist/db/migrations/021_recommendation_feedback.sql +25 -0
- package/dist/detector/d7-loop-analysis.js +142 -0
- package/dist/detector/detectors/d7_loop_retry_waste.js +14 -127
- package/dist/effects/engine.js +27 -10
- package/dist/effects/index.js +1 -0
- package/dist/effects/registry.js +87 -6
- package/dist/effects/scope-identity.js +19 -0
- package/dist/effects/sql-observer.js +322 -8
- package/dist/ingest/boot-progress.js +76 -0
- package/dist/ingest/ingestor.js +352 -82
- package/dist/ingest/parser.js +19 -3
- package/dist/ingest/tail.js +130 -20
- package/dist/ingest/types.js +1 -1
- package/dist/query/api/decision-ranker-types.js +6 -0
- package/dist/query/api/decision-ranker.js +393 -0
- package/dist/query/api/effect-service.js +2 -0
- package/dist/query/api/esf-observations.js +117 -62
- package/dist/query/api/evidence-packet-types.js +3 -0
- package/dist/query/api/evidence-packet.js +316 -0
- package/dist/query/api/feedback-store.js +286 -0
- package/dist/query/api/index.js +1 -1
- package/dist/query/api/opportunity-composer-types.js +3 -0
- package/dist/query/api/opportunity-composer.js +303 -0
- package/dist/query/api/overview.js +42 -1
- package/dist/query/api/reported-work.js +46 -0
- package/dist/query/api/session-drivers.js +1 -1
- package/dist/query/api/spend-flavor.js +22 -15
- package/dist/query/api/trends.js +86 -6
- package/dist/query/api/work-records.js +85 -0
- package/dist/query/settings-store.js +11 -0
- package/dist/query/spend.js +27 -13
- package/dist/query/trends.js +27 -0
- package/dist/ui/assets/{BarChart-CIjCNfbI.js → BarChart-C9vtAcml.js} +1 -1
- package/dist/ui/assets/{BriefsPage-DIU8iFjV.js → BriefsPage-DLp1pInz.js} +2 -2
- package/dist/ui/assets/{CacheWriteSpikesChart-1Nfnf7OX.js → CacheWriteSpikesChart-BlflM-xW.js} +1 -1
- package/dist/ui/assets/{CartesianChart-DEnDOGKj.js → CartesianChart-0_RUWlzc.js} +1 -1
- package/dist/ui/assets/{Chip-s-32Dh3o.js → Chip-BQ4UlaAL.js} +1 -1
- package/dist/ui/assets/{ComposedChart-DUw8Y79v.js → ComposedChart-CIG9CS0j.js} +1 -1
- package/dist/ui/assets/{EmptyState-B8vpug3_.js → EmptyState-CjBh_Y06.js} +1 -1
- package/dist/ui/assets/FlavorDecomposition-Cb1Et9e_.js +1 -0
- package/dist/ui/assets/{FrictionCell-X75fKRBK.js → FrictionCell-BAcKJbGq.js} +1 -1
- package/dist/ui/assets/{GlossaryPage-D1tYumqL.js → GlossaryPage-C0nFEAdc.js} +1 -1
- package/dist/ui/assets/HotSessionsPage-C6obf1m9.js +1 -0
- package/dist/ui/assets/InfoTip-Dc4F2ylc.js +1 -0
- package/dist/ui/assets/{Legend-CB7sqqJH.js → Legend-Ck2QXclg.js} +1 -1
- package/dist/ui/assets/{Line-B9XY30uT.js → Line-CSuEmMnn.js} +1 -1
- package/dist/ui/assets/Modal-C5VeGRjW.js +1 -0
- package/dist/ui/assets/ObservationEvidence-Bhnk-xkX.css +1 -0
- package/dist/ui/assets/ObservationEvidence-CP8cvr_5.js +1 -0
- package/dist/ui/assets/OverviewPage-CCyQ2Oxn.js +2 -0
- package/dist/ui/assets/RecommendationsPage-Bs1B1AJ0.js +2 -0
- package/dist/ui/assets/{Scatter-BSWm_yIZ.js → Scatter-BiTrTZ1e.js} +1 -1
- package/dist/ui/assets/SectionHeader-BcQQCuZA.js +1 -0
- package/dist/ui/assets/SessionDetailPage-CcPL36Cv.js +1 -0
- package/dist/ui/assets/SettingsPage-CzAe6Xf1.js +25 -0
- package/dist/ui/assets/Skeleton-D1ey4h0q.js +1 -0
- package/dist/ui/assets/SpendPercentileChip-DiTZ1xoI.js +1 -0
- package/dist/ui/assets/{TrendChart-BSLwlBiq.js → TrendChart-B51LSzds.js} +1 -1
- package/dist/ui/assets/WorkspaceDetailPage-B4LWopuo.js +1 -0
- package/dist/ui/assets/WorkspacesPage-m4Km-zOE.js +1 -0
- package/dist/ui/assets/chart-theme-BAFdtBab.js +1 -0
- package/dist/ui/assets/{graphicalItemSelectors-0-Ue_y1f.js → graphicalItemSelectors-yTOJ8llA.js} +1 -1
- package/dist/ui/assets/index-DTmPINeX.js +41 -0
- package/dist/ui/assets/index-qkEcvt6k.css +1 -0
- package/dist/ui/assets/{prompt-templates-CVzMex9L.js → prompt-templates-CVAAu0IZ.js} +1 -1
- package/dist/ui/assets/short-id-C12NbqLn.js +1 -0
- package/dist/ui/assets/{useExperimentalActions-aI00AFZv.js → useExperimentalActions-BVXB2b0W.js} +1 -1
- package/dist/ui/assets/workspace-names-DW0HTwW6.js +1 -0
- package/dist/ui/index.html +2 -2
- package/dist/work-records/allocation.js +242 -0
- package/dist/work-records/ids.js +80 -0
- package/dist/work-records/index.js +4 -0
- package/dist/work-records/store.js +376 -0
- package/dist/work-records/types.js +29 -0
- package/dist/work-records/validation.js +71 -0
- package/package.json +4 -3
- package/dist/ui/assets/FlavorDecomposition-CkHX3M-U.js +0 -1
- package/dist/ui/assets/HotSessionsPage-8HlRe0VB.js +0 -1
- package/dist/ui/assets/InfoTip-CL8RC4vz.js +0 -1
- package/dist/ui/assets/OverviewPage-DL4joBBE.js +0 -2
- package/dist/ui/assets/RecommendationsPage-NkfH17eB.js +0 -2
- package/dist/ui/assets/SessionDetailPage-Ceqt7poO.js +0 -1
- package/dist/ui/assets/SettingsPage-ZK4ii5h_.js +0 -25
- package/dist/ui/assets/Skeleton-Dp_m79B8.js +0 -1
- package/dist/ui/assets/SpendPercentileChip-CzY6h1gP.js +0 -1
- package/dist/ui/assets/WorkspaceDetailPage-DFOdEB39.js +0 -1
- package/dist/ui/assets/WorkspacesPage-Doct6meK.js +0 -1
- package/dist/ui/assets/chart-theme-DYgw_Tac.js +0 -1
- package/dist/ui/assets/index-D_IJsvkJ.css +0 -1
- package/dist/ui/assets/index-DhFiA8Ch.js +0 -41
- package/dist/ui/assets/workspace-label-CysPJNc0.js +0 -1
package/dist/daemon/router.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
|
-
|
|
2
|
-
* src/daemon/router.ts — minimal API route dispatcher.
|
|
3
|
-
*
|
|
4
|
-
* Routes /api/* requests to the LocalQueryAPI stubs.
|
|
5
|
-
* WP2 and WP4 replace the stub calls with real implementations.
|
|
6
|
-
*
|
|
7
|
-
* All responses are JSON with no permissive CORS headers.
|
|
8
|
-
*/
|
|
1
|
+
import { getReportedWork } from "../query/api/reported-work.js";
|
|
9
2
|
import { getPractices } from "../detector/practice-registry.js";
|
|
3
|
+
import { getBootProgress } from "../ingest/boot-progress.js";
|
|
10
4
|
import { getOAuthStatus } from "../oauth/credentials.js";
|
|
11
5
|
import { getGithubTokenStatus } from "../outcomes/github/credential.js";
|
|
12
6
|
import { manualLink, manualUnlink } from "../outcomes/linker.js";
|
|
@@ -17,15 +11,18 @@ import { getDeliveryMetrics } from "../query/api/delivery.js";
|
|
|
17
11
|
import { EffectRequestError, listEffectEvidence, mutateEffectCycle, trackCompletedChange, } from "../query/api/effect-service.js";
|
|
18
12
|
import { getClosureProxy } from "../query/api/effectiveness.js";
|
|
19
13
|
import { getEfficiencyHeadroom } from "../query/api/efficiency-headroom.js";
|
|
14
|
+
import { getEsfObservations, getSessionEsfObservations } from "../query/api/esf-observations.js";
|
|
15
|
+
import { FeedbackError, deleteFeedbackRoute, getGoalRoute, listFeedbackRoute, resetGoalRoute, setFeedbackRoute, setGoalRoute, undoFeedbackRoute, } from "../query/api/feedback-store.js";
|
|
20
16
|
import { getHeadroomTrend } from "../query/api/headroom-trend.js";
|
|
21
17
|
import { getHotSessions } from "../query/api/hot-sessions.js";
|
|
22
|
-
import { adoptRecommendation, calibrateBytesPerToken, calibrateLimit, confirmApplyJob, dismissRecommendation, getApplyJob, getContextBudget, getContextComposition, getGlobalOverview, getHookConfig, getIdleSessions, getLinkageRate, getLoopGuard, getSession, getSessionDrivers, getSettings, getSuccessRate, getTurnTimeline, getWorkspaceOutcomeDetail, installHookRoute, listLedger, listLiveSessions, listRecommendations, listSessions, listWorkspaceOutcomes, listWorkspaces, openTerminalForRec, resetDatabase, rollbackApplyJob, startApplyJob, uninstallHookRoute, updateHookConfig, updateSettings, } from "../query/api/index.js";
|
|
18
|
+
import { adoptRecommendation, calibrateBytesPerToken, calibrateLimit, confirmApplyJob, dismissRecommendation, getApplyJob, getContextBudget, getContextComposition, getGlobalOverview, getHookConfig, getIdleSessions, getLinkageRate, getLoopGuard, getSession, getSessionDrivers, getSettings, getSuccessRate, getTurnTimeline, getWorkspaceOutcomeDetail, installHookRoute, listLedger, listLiveSessions, listRecommendations, listSessions, listWorkspaceNames, listWorkspaceOutcomes, listWorkspaces, openTerminalForRec, resetDatabase, rollbackApplyJob, startApplyJob, uninstallHookRoute, updateHookConfig, updateSettings, } from "../query/api/index.js";
|
|
23
19
|
import { getOffloadShare } from "../query/api/offload-share.js";
|
|
24
20
|
import { getReport, listReports } from "../query/api/reports.js";
|
|
25
21
|
import { getSelfChurn } from "../query/api/self-churn.js";
|
|
26
22
|
import { getSessionSpendPercentile, getWeeklySelfPercentile, } from "../query/api/self-percentiles.js";
|
|
27
23
|
import { getFlavorDecomposition } from "../query/api/spend-flavor.js";
|
|
28
24
|
import { getCacheWriteTrend, getTrends } from "../query/api/trends.js";
|
|
25
|
+
import { WorkRecordError, attachWorkRecordContextRoute, attachWorkRecordSessionRoute, closeoutWorkRecordRoute, createWorkRecordRoute, deleteWorkRecordRoute, detachWorkRecordContextRoute, detachWorkRecordSessionRoute, editWorkRecordRoute, getWorkAllocation, getWorkAllocationClaimKinds, getWorkRecord, issueWorkRecordId, listWorkRecords, recomputeWorkAllocationRoute, setWorkRecordArchivedRoute, workRecordResponse, } from "../query/api/work-records.js";
|
|
29
26
|
import { getSettingsData } from "../query/settings-store.js";
|
|
30
27
|
import { getScanStatus, isReady } from "./readiness.js";
|
|
31
28
|
function sendJson(res, status, body) {
|
|
@@ -36,6 +33,39 @@ function sendJson(res, status, body) {
|
|
|
36
33
|
});
|
|
37
34
|
res.end(payload);
|
|
38
35
|
}
|
|
36
|
+
function sendFeedbackError(res, error) {
|
|
37
|
+
if (error instanceof FeedbackError) {
|
|
38
|
+
sendJson(res, error.status, { error: error.message, code: error.code });
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
// Oversized body (readBody) rejects with EffectRequestError(400, …).
|
|
42
|
+
if (error instanceof EffectRequestError) {
|
|
43
|
+
sendJson(res, error.status, { error: error.message, code: "INVALID_REQUEST" });
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
sendJson(res, 500, { error: "Internal error" });
|
|
47
|
+
}
|
|
48
|
+
/** Read + JSON-parse a write body, then run `run` and send its enveloped result. */
|
|
49
|
+
function withJsonBody(req, res, run) {
|
|
50
|
+
readBody(req, 4096)
|
|
51
|
+
.then((raw) => {
|
|
52
|
+
let body;
|
|
53
|
+
try {
|
|
54
|
+
body = JSON.parse(raw);
|
|
55
|
+
}
|
|
56
|
+
catch {
|
|
57
|
+
sendJson(res, 400, { error: "Invalid JSON body", code: "INVALID_REQUEST" });
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
try {
|
|
61
|
+
sendJson(res, 200, run(body));
|
|
62
|
+
}
|
|
63
|
+
catch (error) {
|
|
64
|
+
sendFeedbackError(res, error);
|
|
65
|
+
}
|
|
66
|
+
})
|
|
67
|
+
.catch((error) => sendFeedbackError(res, error));
|
|
68
|
+
}
|
|
39
69
|
function readBody(req, maxBytes) {
|
|
40
70
|
return new Promise((resolve, reject) => {
|
|
41
71
|
const chunks = [];
|
|
@@ -134,10 +164,96 @@ function jobRoute(pathname) {
|
|
|
134
164
|
const action = m[2] === "confirm" || m[2] === "rollback" ? m[2] : "get";
|
|
135
165
|
return { jobId: decodeURIComponent(m[1]), action };
|
|
136
166
|
}
|
|
167
|
+
function workRecordId(pathname) {
|
|
168
|
+
const match = pathname.match(/^\/api\/work-records\/([^/]+)$/);
|
|
169
|
+
return match?.[1] === undefined ? null : decodeURIComponent(match[1]);
|
|
170
|
+
}
|
|
171
|
+
function listSavedCostReports(db, workspaceId) {
|
|
172
|
+
return db
|
|
173
|
+
.prepare(`SELECT r.allocation_revision_id, r.created_at, r.cohort_from, r.cohort_to,
|
|
174
|
+
SUM(CASE WHEN a.disposition = 'OWNED' THEN 1 ELSE 0 END) AS allocated_session_count,
|
|
175
|
+
COUNT(a.session_id) AS eligible_session_count
|
|
176
|
+
FROM work_allocation_revisions r
|
|
177
|
+
LEFT JOIN work_session_allocations a ON a.allocation_revision_id = r.allocation_revision_id
|
|
178
|
+
WHERE r.workspace_id = ?
|
|
179
|
+
GROUP BY r.allocation_revision_id
|
|
180
|
+
ORDER BY r.created_at DESC, r.allocation_revision_id DESC`)
|
|
181
|
+
.all(workspaceId);
|
|
182
|
+
}
|
|
183
|
+
function workRecordAction(pathname) {
|
|
184
|
+
const match = pathname.match(/^\/api\/work-records\/([^/]+)\/(edit|closeout|archive|reopen|sessions|contexts|delete)(?:\/([^/]+)\/detach)?$/);
|
|
185
|
+
if (match?.[1] === undefined || match[2] === undefined)
|
|
186
|
+
return null;
|
|
187
|
+
const result = { recordId: decodeURIComponent(match[1]), action: match[2] };
|
|
188
|
+
return match[3] === undefined ? result : { ...result, relatedId: decodeURIComponent(match[3]) };
|
|
189
|
+
}
|
|
190
|
+
function workRecordBody(raw, recordId, related) {
|
|
191
|
+
let body;
|
|
192
|
+
try {
|
|
193
|
+
body = JSON.parse(raw);
|
|
194
|
+
}
|
|
195
|
+
catch {
|
|
196
|
+
throw new WorkRecordError(400, "INVALID_REQUEST", "Invalid JSON body");
|
|
197
|
+
}
|
|
198
|
+
if (body === null || typeof body !== "object" || Array.isArray(body)) {
|
|
199
|
+
throw new WorkRecordError(400, "INVALID_REQUEST", "request body must be an object");
|
|
200
|
+
}
|
|
201
|
+
const result = { ...body };
|
|
202
|
+
if (recordId !== undefined) {
|
|
203
|
+
if (result.work_record_id !== undefined && result.work_record_id !== recordId) {
|
|
204
|
+
throw new WorkRecordError(400, "INVALID_REQUEST", "work_record_id must match the route");
|
|
205
|
+
}
|
|
206
|
+
result.work_record_id = recordId;
|
|
207
|
+
}
|
|
208
|
+
if (related !== undefined) {
|
|
209
|
+
if (result[related.key] !== undefined && result[related.key] !== related.value) {
|
|
210
|
+
throw new WorkRecordError(400, "INVALID_REQUEST", `${related.key} must match the route`);
|
|
211
|
+
}
|
|
212
|
+
result[related.key] = related.value;
|
|
213
|
+
}
|
|
214
|
+
return result;
|
|
215
|
+
}
|
|
216
|
+
function sendWorkRecordError(res, error) {
|
|
217
|
+
if (error instanceof EffectRequestError) {
|
|
218
|
+
sendJson(res, error.status, { error: error.message, code: "INVALID_REQUEST" });
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
if (error instanceof WorkRecordError) {
|
|
222
|
+
sendJson(res, error.status, { error: error.message, code: error.code });
|
|
223
|
+
return;
|
|
224
|
+
}
|
|
225
|
+
sendJson(res, 500, { error: "Internal error" });
|
|
226
|
+
}
|
|
137
227
|
export function handleApiRequest(_db, req, res, method, url) {
|
|
138
228
|
// Strip query string for routing.
|
|
139
229
|
const pathname = url.split("?")[0] ?? url;
|
|
140
230
|
try {
|
|
231
|
+
if (method === "GET" && pathname === "/api/work-records/summary") {
|
|
232
|
+
const params = new URLSearchParams(url.split("?")[1] ?? "");
|
|
233
|
+
sendJson(res, 200, getReportedWork(_db, params.get("workspace_id") || null));
|
|
234
|
+
return;
|
|
235
|
+
}
|
|
236
|
+
// GET /api/esf/observations — complete selected-turn accounting cohort.
|
|
237
|
+
if (method === "GET" && pathname === "/api/esf/observations") {
|
|
238
|
+
const params = new URLSearchParams(url.split("?")[1] ?? "");
|
|
239
|
+
const window = resolveWindow(parseWindowFilter(url));
|
|
240
|
+
if (Date.parse(window.from) >= Date.parse(window.to)) {
|
|
241
|
+
sendJson(res, 400, { error: "from must precede to" });
|
|
242
|
+
return;
|
|
243
|
+
}
|
|
244
|
+
sendJson(res, 200, getEsfObservations(_db, {
|
|
245
|
+
workspaceId: params.get("workspace_id") || null,
|
|
246
|
+
...window,
|
|
247
|
+
}));
|
|
248
|
+
return;
|
|
249
|
+
}
|
|
250
|
+
// Full-session aggregate: never derive this from a paginated timeline.
|
|
251
|
+
const sessionObservationId = pathname.match(/^\/api\/esf\/session-observations\/([^/]+)$/)?.[1];
|
|
252
|
+
if (method === "GET" && sessionObservationId !== undefined) {
|
|
253
|
+
const evidence = getSessionEsfObservations(_db, decodeURIComponent(sessionObservationId));
|
|
254
|
+
sendJson(res, evidence.data === null ? 404 : 200, evidence);
|
|
255
|
+
return;
|
|
256
|
+
}
|
|
141
257
|
// GET /api/esf/effects — independently keyset-paged versioned and legacy evidence.
|
|
142
258
|
if (method === "GET" && pathname === "/api/esf/effects") {
|
|
143
259
|
const params = new URLSearchParams(url.split("?")[1] ?? "");
|
|
@@ -179,6 +295,172 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
179
295
|
}));
|
|
180
296
|
return;
|
|
181
297
|
}
|
|
298
|
+
// ESF3 work records are local-only, structured records. The HTTP token gate
|
|
299
|
+
// in http.ts covers every mutation and ID issuance route below.
|
|
300
|
+
if (method === "GET" && pathname === "/api/work-records") {
|
|
301
|
+
const params = new URLSearchParams(url.split("?")[1] ?? "");
|
|
302
|
+
const workspaceId = params.get("workspace_id");
|
|
303
|
+
const includeArchived = params.get("include_archived");
|
|
304
|
+
if (workspaceId === null ||
|
|
305
|
+
workspaceId.length === 0 ||
|
|
306
|
+
(includeArchived !== null && includeArchived !== "true" && includeArchived !== "false")) {
|
|
307
|
+
sendJson(res, 400, { error: "workspace_id and include_archived are invalid" });
|
|
308
|
+
return;
|
|
309
|
+
}
|
|
310
|
+
const records = listWorkRecords(_db, workspaceId, includeArchived === "true");
|
|
311
|
+
sendJson(res, 200, workRecordResponse(records, { n: records.length, workspaceId }));
|
|
312
|
+
return;
|
|
313
|
+
}
|
|
314
|
+
if (method === "GET" && pathname === "/api/work-records/allocations") {
|
|
315
|
+
const workspaceId = new URLSearchParams(url.split("?")[1] ?? "").get("workspace_id");
|
|
316
|
+
if (workspaceId === null || workspaceId.length === 0) {
|
|
317
|
+
sendJson(res, 400, { error: "workspace_id is required" });
|
|
318
|
+
return;
|
|
319
|
+
}
|
|
320
|
+
const reports = listSavedCostReports(_db, workspaceId);
|
|
321
|
+
sendJson(res, 200, workRecordResponse(reports, { n: reports.length, workspaceId }));
|
|
322
|
+
return;
|
|
323
|
+
}
|
|
324
|
+
if (method === "GET" && pathname.startsWith("/api/work-records/allocations/")) {
|
|
325
|
+
const allocationId = pathname.slice("/api/work-records/allocations/".length);
|
|
326
|
+
if (allocationId.length === 0 || allocationId.includes("/")) {
|
|
327
|
+
sendJson(res, 404, { error: "Not found" });
|
|
328
|
+
return;
|
|
329
|
+
}
|
|
330
|
+
try {
|
|
331
|
+
const allocation = getWorkAllocation(_db, decodeURIComponent(allocationId));
|
|
332
|
+
sendJson(res, 200, workRecordResponse(allocation, {
|
|
333
|
+
n: allocation.eligible_session_count,
|
|
334
|
+
workspaceId: allocation.workspace_id,
|
|
335
|
+
claimKind: "EXPERIMENTAL",
|
|
336
|
+
meta: {
|
|
337
|
+
window: { from: allocation.cohort_from, to: allocation.cohort_to },
|
|
338
|
+
qualification: {
|
|
339
|
+
provisional_excluded: true,
|
|
340
|
+
unpriced_turns: allocation.unpriced_turn_count,
|
|
341
|
+
claim_kinds_count: getWorkAllocationClaimKinds(_db, allocation.allocation_revision_id),
|
|
342
|
+
note: "Frozen ESF3 allocation; unpriced turns are not zero cost.",
|
|
343
|
+
},
|
|
344
|
+
},
|
|
345
|
+
}));
|
|
346
|
+
}
|
|
347
|
+
catch (error) {
|
|
348
|
+
sendWorkRecordError(res, error);
|
|
349
|
+
}
|
|
350
|
+
return;
|
|
351
|
+
}
|
|
352
|
+
const recordId = workRecordId(pathname);
|
|
353
|
+
if (method === "GET" && recordId !== null) {
|
|
354
|
+
try {
|
|
355
|
+
const record = getWorkRecord(_db, recordId);
|
|
356
|
+
sendJson(res, 200, workRecordResponse(record, { workspaceId: record.workspace_id }));
|
|
357
|
+
}
|
|
358
|
+
catch (error) {
|
|
359
|
+
sendWorkRecordError(res, error);
|
|
360
|
+
}
|
|
361
|
+
return;
|
|
362
|
+
}
|
|
363
|
+
if (method === "POST" && pathname === "/api/work-records/ids") {
|
|
364
|
+
readBody(req, 1024)
|
|
365
|
+
.then((raw) => {
|
|
366
|
+
try {
|
|
367
|
+
sendJson(res, 200, workRecordResponse(issueWorkRecordId(_db, workRecordBody(raw))));
|
|
368
|
+
}
|
|
369
|
+
catch (error) {
|
|
370
|
+
sendWorkRecordError(res, error);
|
|
371
|
+
}
|
|
372
|
+
})
|
|
373
|
+
.catch((error) => sendWorkRecordError(res, error));
|
|
374
|
+
return;
|
|
375
|
+
}
|
|
376
|
+
if (method === "POST" && pathname === "/api/work-records") {
|
|
377
|
+
readBody(req, 4096)
|
|
378
|
+
.then((raw) => {
|
|
379
|
+
try {
|
|
380
|
+
const result = createWorkRecordRoute(_db, workRecordBody(raw));
|
|
381
|
+
sendJson(res, 200, workRecordResponse(result, { workspaceId: result.record.workspace_id }));
|
|
382
|
+
}
|
|
383
|
+
catch (error) {
|
|
384
|
+
sendWorkRecordError(res, error);
|
|
385
|
+
}
|
|
386
|
+
})
|
|
387
|
+
.catch((error) => sendWorkRecordError(res, error));
|
|
388
|
+
return;
|
|
389
|
+
}
|
|
390
|
+
if (method === "POST" && pathname === "/api/work-records/allocations/recompute") {
|
|
391
|
+
readBody(req, 4096)
|
|
392
|
+
.then((raw) => {
|
|
393
|
+
try {
|
|
394
|
+
const result = recomputeWorkAllocationRoute(_db, workRecordBody(raw));
|
|
395
|
+
sendJson(res, 200, workRecordResponse(result, {
|
|
396
|
+
n: result.allocation.eligible_session_count,
|
|
397
|
+
workspaceId: result.allocation.workspace_id,
|
|
398
|
+
claimKind: "EXPERIMENTAL",
|
|
399
|
+
meta: {
|
|
400
|
+
window: {
|
|
401
|
+
from: result.allocation.cohort_from,
|
|
402
|
+
to: result.allocation.cohort_to,
|
|
403
|
+
},
|
|
404
|
+
qualification: {
|
|
405
|
+
provisional_excluded: true,
|
|
406
|
+
unpriced_turns: result.allocation.unpriced_turn_count,
|
|
407
|
+
claim_kinds_count: getWorkAllocationClaimKinds(_db, result.allocation.allocation_revision_id),
|
|
408
|
+
note: "Frozen ESF3 allocation; unpriced turns are not zero cost.",
|
|
409
|
+
},
|
|
410
|
+
},
|
|
411
|
+
}));
|
|
412
|
+
}
|
|
413
|
+
catch (error) {
|
|
414
|
+
sendWorkRecordError(res, error);
|
|
415
|
+
}
|
|
416
|
+
})
|
|
417
|
+
.catch((error) => sendWorkRecordError(res, error));
|
|
418
|
+
return;
|
|
419
|
+
}
|
|
420
|
+
const recordAction = workRecordAction(pathname);
|
|
421
|
+
if (recordAction !== null &&
|
|
422
|
+
((method === "POST" && recordAction.action !== "delete") ||
|
|
423
|
+
(method === "DELETE" && recordAction.action === "delete"))) {
|
|
424
|
+
readBody(req, 4096)
|
|
425
|
+
.then((raw) => {
|
|
426
|
+
try {
|
|
427
|
+
const related = recordAction.relatedId === undefined
|
|
428
|
+
? undefined
|
|
429
|
+
: {
|
|
430
|
+
key: recordAction.action === "contexts" ? "context_ref_id" : "session_id",
|
|
431
|
+
value: recordAction.relatedId,
|
|
432
|
+
};
|
|
433
|
+
const body = workRecordBody(raw, recordAction.recordId, related);
|
|
434
|
+
const result = recordAction.action === "edit"
|
|
435
|
+
? editWorkRecordRoute(_db, body)
|
|
436
|
+
: recordAction.action === "closeout"
|
|
437
|
+
? closeoutWorkRecordRoute(_db, body)
|
|
438
|
+
: recordAction.action === "archive"
|
|
439
|
+
? setWorkRecordArchivedRoute(_db, body, true)
|
|
440
|
+
: recordAction.action === "reopen"
|
|
441
|
+
? setWorkRecordArchivedRoute(_db, body, false)
|
|
442
|
+
: recordAction.action === "sessions" && recordAction.relatedId === undefined
|
|
443
|
+
? attachWorkRecordSessionRoute(_db, body)
|
|
444
|
+
: recordAction.action === "sessions"
|
|
445
|
+
? detachWorkRecordSessionRoute(_db, body)
|
|
446
|
+
: recordAction.action === "contexts" &&
|
|
447
|
+
recordAction.relatedId === undefined
|
|
448
|
+
? attachWorkRecordContextRoute(_db, body)
|
|
449
|
+
: recordAction.action === "contexts"
|
|
450
|
+
? detachWorkRecordContextRoute(_db, body)
|
|
451
|
+
: deleteWorkRecordRoute(_db, body);
|
|
452
|
+
const workspaceId = "record" in result ? result.record.workspace_id : undefined;
|
|
453
|
+
sendJson(res, 200, workspaceId === undefined
|
|
454
|
+
? workRecordResponse(result)
|
|
455
|
+
: workRecordResponse(result, { workspaceId }));
|
|
456
|
+
}
|
|
457
|
+
catch (error) {
|
|
458
|
+
sendWorkRecordError(res, error);
|
|
459
|
+
}
|
|
460
|
+
})
|
|
461
|
+
.catch((error) => sendWorkRecordError(res, error));
|
|
462
|
+
return;
|
|
463
|
+
}
|
|
182
464
|
// GET /api/overview
|
|
183
465
|
if (method === "GET" && pathname === "/api/overview") {
|
|
184
466
|
sendJson(res, 200, getGlobalOverview(parseWindowFilter(url)));
|
|
@@ -275,6 +557,11 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
275
557
|
sendJson(res, 200, listWorkspaces(parseWindowFilter(url)));
|
|
276
558
|
return;
|
|
277
559
|
}
|
|
560
|
+
// GET /api/workspace-names (unwindowed naming metadata for all workspaces — UIR-8)
|
|
561
|
+
if (method === "GET" && pathname === "/api/workspace-names") {
|
|
562
|
+
sendJson(res, 200, listWorkspaceNames());
|
|
563
|
+
return;
|
|
564
|
+
}
|
|
278
565
|
// GET /api/delivery (delivery proxy metrics across all workspaces)
|
|
279
566
|
if (method === "GET" && pathname === "/api/delivery") {
|
|
280
567
|
const { from, to } = resolveWindow(parseWindowFilter(url));
|
|
@@ -574,6 +861,46 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
574
861
|
.catch(() => sendJson(res, 500, { error: "Internal error reading request body" }));
|
|
575
862
|
return;
|
|
576
863
|
}
|
|
864
|
+
// ── RIQ3 relevance feedback + goal preference (migration 021) ──────────
|
|
865
|
+
// Local, enum-only, SEC-101. Writes are CSRF + session-token gated in http.ts.
|
|
866
|
+
if (method === "GET" && pathname === "/api/recommendations/feedback") {
|
|
867
|
+
const scopeKey = new URLSearchParams(url.split("?")[1] ?? "").get("scope_key");
|
|
868
|
+
if (!scopeKey) {
|
|
869
|
+
sendJson(res, 400, { error: "scope_key is required", code: "INVALID_REQUEST" });
|
|
870
|
+
return;
|
|
871
|
+
}
|
|
872
|
+
sendJson(res, 200, listFeedbackRoute(_db, scopeKey));
|
|
873
|
+
return;
|
|
874
|
+
}
|
|
875
|
+
if (method === "POST" && pathname === "/api/recommendations/feedback") {
|
|
876
|
+
withJsonBody(req, res, (body) => setFeedbackRoute(_db, body));
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
if (method === "POST" && pathname === "/api/recommendations/feedback/undo") {
|
|
880
|
+
withJsonBody(req, res, (body) => undoFeedbackRoute(_db, body));
|
|
881
|
+
return;
|
|
882
|
+
}
|
|
883
|
+
if (method === "DELETE" && pathname === "/api/recommendations/feedback") {
|
|
884
|
+
withJsonBody(req, res, (body) => deleteFeedbackRoute(_db, body));
|
|
885
|
+
return;
|
|
886
|
+
}
|
|
887
|
+
if (method === "GET" && pathname === "/api/recommendations/goal") {
|
|
888
|
+
const scopeKey = new URLSearchParams(url.split("?")[1] ?? "").get("scope_key");
|
|
889
|
+
if (!scopeKey) {
|
|
890
|
+
sendJson(res, 400, { error: "scope_key is required", code: "INVALID_REQUEST" });
|
|
891
|
+
return;
|
|
892
|
+
}
|
|
893
|
+
sendJson(res, 200, getGoalRoute(_db, scopeKey));
|
|
894
|
+
return;
|
|
895
|
+
}
|
|
896
|
+
if (method === "POST" && pathname === "/api/recommendations/goal") {
|
|
897
|
+
withJsonBody(req, res, (body) => setGoalRoute(_db, body));
|
|
898
|
+
return;
|
|
899
|
+
}
|
|
900
|
+
if (method === "POST" && pathname === "/api/recommendations/goal/reset") {
|
|
901
|
+
withJsonBody(req, res, (body) => resetGoalRoute(_db, body));
|
|
902
|
+
return;
|
|
903
|
+
}
|
|
577
904
|
// GET /api/settings
|
|
578
905
|
if (method === "GET" && pathname === "/api/settings") {
|
|
579
906
|
sendJson(res, 200, getSettings());
|
|
@@ -796,6 +1123,9 @@ export function handleApiRequest(_db, req, res, method, url) {
|
|
|
796
1123
|
files_parsed: parser_health.files_parsed,
|
|
797
1124
|
lines_quarantined: parser_health.lines_quarantined,
|
|
798
1125
|
...getScanStatus(),
|
|
1126
|
+
// BOOT-1: boot-progress fields exist ONLY pre-ready; the steady-state
|
|
1127
|
+
// /api/status shape is unchanged (UI fixtures must not require them).
|
|
1128
|
+
...(isReady() ? {} : getBootProgress()),
|
|
799
1129
|
});
|
|
800
1130
|
return;
|
|
801
1131
|
}
|