@tangle-network/agent-app 0.43.76 → 0.44.1
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 +1 -1
- package/dist/DesignCanvas-Z362I3R3.js +10 -0
- package/dist/{DesignCanvasEditor-FJIVYOA2.js → DesignCanvasEditor-ZFE2W4OJ.js} +4 -4
- package/dist/assets/index.d.ts +4 -4
- package/dist/assets/index.js +223 -11
- package/dist/assets/index.js.map +1 -1
- package/dist/assistant/index.d.ts +7 -7
- package/dist/assistant/index.js +3 -3
- package/dist/assistant/index.js.map +1 -1
- package/dist/{attachment-validation-Dv1A_Puy.d.ts → attachment-validation-DRuLEjAE.d.ts} +1 -1
- package/dist/{auth-_FU8w01b.d.ts → auth-anc7mv2W.d.ts} +2 -2
- package/dist/chat-routes/index.d.ts +14 -11
- package/dist/chat-routes/index.js +16 -17
- package/dist/chat-routes/index.js.map +1 -1
- package/dist/chat-store/index.d.ts +26 -7
- package/dist/chat-store/index.js +14 -5
- package/dist/chat-store/index.js.map +1 -1
- package/dist/{chunk-ROLFDOMJ.js → chunk-4TI7QWLW.js} +3 -3
- package/dist/{chunk-3JUTOVUH.js → chunk-6KMAM7BL.js} +1 -1
- package/dist/chunk-6KMAM7BL.js.map +1 -0
- package/dist/{chunk-WL7XHLDK.js → chunk-ICOHEZK6.js} +77 -5
- package/dist/chunk-ICOHEZK6.js.map +1 -0
- package/dist/{chunk-22GLESKE.js → chunk-O63WWYET.js} +2 -4
- package/dist/chunk-O63WWYET.js.map +1 -0
- package/dist/{chunk-E7N67FYI.js → chunk-Q3FONVAK.js} +5 -5
- package/dist/{chunk-E7N67FYI.js.map → chunk-Q3FONVAK.js.map} +1 -1
- package/dist/{chunk-RXOTWZ4G.js → chunk-TKRI463Z.js} +4 -4
- package/dist/chunk-TKRI463Z.js.map +1 -0
- package/dist/config/index.d.ts +1 -1
- package/dist/config/index.js +123 -4
- package/dist/config/index.js.map +1 -1
- package/dist/{contract-B3h7peV3.d.ts → contract-CEewO6DI.d.ts} +1 -1
- package/dist/design-canvas/index.d.ts +4 -4
- package/dist/design-canvas-react/index.js +7 -7
- package/dist/design-canvas-react/lazy.js +1 -1
- package/dist/eval/index.d.ts +1 -1
- package/dist/eval/index.js +22 -8
- package/dist/eval/index.js.map +1 -1
- package/dist/{flow-types-CqomVAUN.d.ts → flow-types-CJxEmaRy.d.ts} +1 -1
- package/dist/integrations/index.js +67 -5
- package/dist/integrations/index.js.map +1 -1
- package/dist/interactions/index.d.ts +6 -4
- package/dist/interactions/index.js +1 -2
- package/dist/knowledge/index.js +63 -4
- package/dist/knowledge/index.js.map +1 -1
- package/dist/knowledge-loop/index.d.ts +1 -1
- package/dist/knowledge-loop/index.js +94 -4
- package/dist/knowledge-loop/index.js.map +1 -1
- package/dist/{mcp-BjPqQ-3C.d.ts → mcp-D3qVzbE1.d.ts} +2 -2
- package/dist/{mcp-rpc-BfUOsalw.d.ts → mcp-rpc-CzU5LWWT.d.ts} +1 -1
- package/dist/missions/index.js +1143 -23
- package/dist/missions/index.js.map +1 -1
- package/dist/{model-DmdkIteM.d.ts → model-CdCDfBA9.d.ts} +1 -1
- package/dist/object-store/index.js +113 -8
- package/dist/object-store/index.js.map +1 -1
- package/dist/{parts-2ymE5cs-.d.ts → parts-Co_lFgLI.d.ts} +3 -3
- package/dist/platform/index.d.ts +1 -1
- package/dist/preflight/cli.d.ts +13 -0
- package/dist/preflight/cli.js +50 -0
- package/dist/preflight/cli.js.map +1 -0
- package/dist/preset-cloudflare/index.d.ts +1 -1
- package/dist/preset-cloudflare/index.js +284 -11
- package/dist/preset-cloudflare/index.js.map +1 -1
- package/dist/{queue-C24V13h9.d.ts → queue-CD5-_VTX.d.ts} +1 -1
- package/dist/redact/index.js +116 -8
- package/dist/redact/index.js.map +1 -1
- package/dist/runtime/index.d.ts +19 -111
- package/dist/runtime/index.js +234 -14
- package/dist/runtime/index.js.map +1 -1
- package/dist/sandbox/index.d.ts +3 -3
- package/dist/sandbox/index.js +2 -3
- package/dist/sequences/index.d.ts +4 -4
- package/dist/stream/index.d.ts +4 -4
- package/dist/{stream-normalizer-QYUl4vnl.d.ts → stream-normalizer-DnuqkZvw.d.ts} +2 -2
- package/dist/tangle/index.js +42 -4
- package/dist/tangle/index.js.map +1 -1
- package/dist/teams/index.js +5 -5
- package/dist/teams/invitations-api.js +4 -4
- package/dist/teams-react/index.js +3 -3
- package/dist/theme/index.js +0 -1
- package/dist/tools/index.d.ts +6 -6
- package/dist/tools/index.js +5 -7
- package/dist/trace/index.d.ts +1 -1
- package/dist/trace/index.js +179 -10
- package/dist/trace/index.js.map +1 -1
- package/dist/{turn-buffer-BaSgxVmK.d.ts → turn-buffer-BcPfhr1_.d.ts} +1 -1
- package/dist/turn-stream/index.d.ts +1 -1
- package/dist/{types-CBRyqijY.d.ts → types-BCxK0wyS.d.ts} +1 -1
- package/dist/{types-CEchbvgz.d.ts → types-XIZqIdX1.d.ts} +1 -1
- package/dist/web-react/index.d.ts +10 -10
- package/dist/web-react/index.js +5 -5
- package/dist/work-product/index.d.ts +4 -4
- package/dist/work-product/index.js +735 -15
- package/dist/work-product/index.js.map +1 -1
- package/dist/work-product-react/index.d.ts +1 -1
- package/dist/work-product-react/index.js.map +1 -1
- package/package.json +4 -19
- package/dist/DesignCanvas-UNL34LS2.js +0 -12
- package/dist/chunk-22GLESKE.js.map +0 -1
- package/dist/chunk-3JUTOVUH.js.map +0 -1
- package/dist/chunk-5EQCITY3.js +0 -20
- package/dist/chunk-5EQCITY3.js.map +0 -1
- package/dist/chunk-5SD6HT2V.js +0 -236
- package/dist/chunk-5SD6HT2V.js.map +0 -1
- package/dist/chunk-5TESA4SN.js +0 -74
- package/dist/chunk-5TESA4SN.js.map +0 -1
- package/dist/chunk-6GQ5DKFK.js +0 -920
- package/dist/chunk-6GQ5DKFK.js.map +0 -1
- package/dist/chunk-7ESQUSAC.js +0 -343
- package/dist/chunk-7ESQUSAC.js.map +0 -1
- package/dist/chunk-7LNGJDNA.js +0 -1
- package/dist/chunk-7LNGJDNA.js.map +0 -1
- package/dist/chunk-AP2UXKUK.js +0 -69
- package/dist/chunk-AP2UXKUK.js.map +0 -1
- package/dist/chunk-FA4XR66Y.js +0 -32
- package/dist/chunk-FA4XR66Y.js.map +0 -1
- package/dist/chunk-FKCB5FTD.js +0 -126
- package/dist/chunk-FKCB5FTD.js.map +0 -1
- package/dist/chunk-GPBRL2XR.js +0 -102
- package/dist/chunk-GPBRL2XR.js.map +0 -1
- package/dist/chunk-HRH7ASAG.js +0 -759
- package/dist/chunk-HRH7ASAG.js.map +0 -1
- package/dist/chunk-HYTIHYNV.js +0 -297
- package/dist/chunk-HYTIHYNV.js.map +0 -1
- package/dist/chunk-LGBO24FL.js +0 -190
- package/dist/chunk-LGBO24FL.js.map +0 -1
- package/dist/chunk-NLSSFLLS.js +0 -1168
- package/dist/chunk-NLSSFLLS.js.map +0 -1
- package/dist/chunk-OSMNYPOG.js +0 -129
- package/dist/chunk-OSMNYPOG.js.map +0 -1
- package/dist/chunk-QYOD3K56.js +0 -124
- package/dist/chunk-QYOD3K56.js.map +0 -1
- package/dist/chunk-RXOTWZ4G.js.map +0 -1
- package/dist/chunk-UAKFCMWK.js +0 -1
- package/dist/chunk-UAKFCMWK.js.map +0 -1
- package/dist/chunk-UKTYKG57.js +0 -48
- package/dist/chunk-UKTYKG57.js.map +0 -1
- package/dist/chunk-UOAYS72M.js +0 -80
- package/dist/chunk-UOAYS72M.js.map +0 -1
- package/dist/chunk-WL7XHLDK.js.map +0 -1
- package/dist/composer/index.d.ts +0 -2
- package/dist/composer/index.js +0 -22
- package/dist/composer/index.js.map +0 -1
- package/dist/core-7qIM7svy.d.ts +0 -21
- package/dist/durable-chat/index.d.ts +0 -464
- package/dist/durable-chat/index.js +0 -59
- package/dist/durable-chat/index.js.map +0 -1
- package/dist/index.d.ts +0 -51
- package/dist/index.js +0 -827
- package/dist/index.js.map +0 -1
- /package/dist/{DesignCanvas-UNL34LS2.js.map → DesignCanvas-Z362I3R3.js.map} +0 -0
- /package/dist/{DesignCanvasEditor-FJIVYOA2.js.map → DesignCanvasEditor-ZFE2W4OJ.js.map} +0 -0
- /package/dist/{chunk-ROLFDOMJ.js.map → chunk-4TI7QWLW.js.map} +0 -0
|
@@ -1,17 +1,3 @@
|
|
|
1
|
-
import {
|
|
2
|
-
EVIDENCE_COVERAGE_CHECK,
|
|
3
|
-
MAX_WORK_PRODUCT_BATCH,
|
|
4
|
-
buildWorkProductTools,
|
|
5
|
-
canTransitionWorkProduct,
|
|
6
|
-
createInMemoryWorkProductStore,
|
|
7
|
-
createWorkProductRoutes,
|
|
8
|
-
createWorkProductService,
|
|
9
|
-
finalizeWorkProductProvenance,
|
|
10
|
-
isWorkProductTerminal,
|
|
11
|
-
stampProvenance,
|
|
12
|
-
validateWorkProductVerdictBody,
|
|
13
|
-
workProductTrustInputs
|
|
14
|
-
} from "../chunk-HRH7ASAG.js";
|
|
15
1
|
import {
|
|
16
2
|
parseReviewQueueItem,
|
|
17
3
|
projectReviewQueue
|
|
@@ -26,7 +12,741 @@ import {
|
|
|
26
12
|
unresolvedBlockingExceptions,
|
|
27
13
|
workProductToPersistedPart
|
|
28
14
|
} from "../chunk-F2CBC4DY.js";
|
|
29
|
-
import
|
|
15
|
+
import {
|
|
16
|
+
ToolInputError,
|
|
17
|
+
defineAppTool
|
|
18
|
+
} from "../chunk-YEFFHORB.js";
|
|
19
|
+
|
|
20
|
+
// src/work-product/service.ts
|
|
21
|
+
var WORK_PRODUCT_TRANSITIONS = {
|
|
22
|
+
draft: /* @__PURE__ */ new Set(["blocked", "ready"]),
|
|
23
|
+
blocked: /* @__PURE__ */ new Set(["draft"]),
|
|
24
|
+
ready: /* @__PURE__ */ new Set(["changes_requested", "approved", "superseded"]),
|
|
25
|
+
changes_requested: /* @__PURE__ */ new Set(["draft", "superseded"]),
|
|
26
|
+
approved: /* @__PURE__ */ new Set(["superseded"]),
|
|
27
|
+
superseded: /* @__PURE__ */ new Set()
|
|
28
|
+
};
|
|
29
|
+
function canTransitionWorkProduct(from, to) {
|
|
30
|
+
return WORK_PRODUCT_TRANSITIONS[from].has(to);
|
|
31
|
+
}
|
|
32
|
+
function isWorkProductTerminal(status) {
|
|
33
|
+
return WORK_PRODUCT_TRANSITIONS[status].size === 0;
|
|
34
|
+
}
|
|
35
|
+
function rejected(error) {
|
|
36
|
+
return { succeeded: false, error, conflict: false };
|
|
37
|
+
}
|
|
38
|
+
function lostRace(id) {
|
|
39
|
+
return { succeeded: false, error: `Work product ${id} changed concurrently`, conflict: true };
|
|
40
|
+
}
|
|
41
|
+
function mergeById(existing, incoming) {
|
|
42
|
+
const merged = existing.slice();
|
|
43
|
+
const indexById = new Map(merged.map((entry, index) => [entry.id, index]));
|
|
44
|
+
for (const entry of incoming) {
|
|
45
|
+
const at = indexById.get(entry.id);
|
|
46
|
+
if (at === void 0) {
|
|
47
|
+
indexById.set(entry.id, merged.length);
|
|
48
|
+
merged.push(entry);
|
|
49
|
+
} else {
|
|
50
|
+
merged[at] = entry;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
return merged;
|
|
54
|
+
}
|
|
55
|
+
function createWorkProductService(options) {
|
|
56
|
+
const { store } = options;
|
|
57
|
+
const now = options.now ?? (() => Date.now());
|
|
58
|
+
const generateId = options.generateId ?? (() => crypto.randomUUID());
|
|
59
|
+
async function appendEvent(record, step, message, metadata = {}) {
|
|
60
|
+
await store.appendEvent({
|
|
61
|
+
workProductId: record.id,
|
|
62
|
+
workspaceId: record.workspaceId,
|
|
63
|
+
step,
|
|
64
|
+
message,
|
|
65
|
+
metadata,
|
|
66
|
+
at: now()
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
async function transition(id, to, patch = {}, eventMeta = {}) {
|
|
70
|
+
const record = await store.load(id);
|
|
71
|
+
if (!record) return rejected(`Work product ${id} not found`);
|
|
72
|
+
const from = record.status;
|
|
73
|
+
if (isWorkProductTerminal(from)) {
|
|
74
|
+
return rejected(`Work product ${id} is terminal (${from}); cannot transition to ${to}`);
|
|
75
|
+
}
|
|
76
|
+
if (!canTransitionWorkProduct(from, to)) {
|
|
77
|
+
return rejected(`Illegal work-product transition ${from} -> ${to} for ${id}`);
|
|
78
|
+
}
|
|
79
|
+
const updated = await store.update(
|
|
80
|
+
id,
|
|
81
|
+
{ status: from, version: record.version },
|
|
82
|
+
{ status: to, updatedAt: now(), ...patch }
|
|
83
|
+
);
|
|
84
|
+
if (!updated) return lostRace(id);
|
|
85
|
+
await appendEvent(updated, `wp.${to}`, `Work product ${from} -> ${to}`, { from, to, ...eventMeta });
|
|
86
|
+
return { succeeded: true, value: updated };
|
|
87
|
+
}
|
|
88
|
+
async function guardedMerge(id, legalStatuses, build, event) {
|
|
89
|
+
const record = await store.load(id);
|
|
90
|
+
if (!record) return rejected(`Work product ${id} not found`);
|
|
91
|
+
if (!legalStatuses.includes(record.status)) {
|
|
92
|
+
return rejected(`Work product ${id} is ${record.status}; expected ${legalStatuses.join("/")}`);
|
|
93
|
+
}
|
|
94
|
+
const updated = await store.update(
|
|
95
|
+
id,
|
|
96
|
+
{ status: record.status, version: record.version },
|
|
97
|
+
{ updatedAt: now(), ...build(record) }
|
|
98
|
+
);
|
|
99
|
+
if (!updated) return lostRace(id);
|
|
100
|
+
await appendEvent(updated, event.step, event.message(updated), event.metadata?.(updated) ?? {});
|
|
101
|
+
return { succeeded: true, value: updated };
|
|
102
|
+
}
|
|
103
|
+
const create = async (input) => {
|
|
104
|
+
const at = now();
|
|
105
|
+
const record = await store.insert(
|
|
106
|
+
{
|
|
107
|
+
id: input.id ?? generateId(),
|
|
108
|
+
workspaceId: input.workspaceId,
|
|
109
|
+
threadId: input.threadId,
|
|
110
|
+
scopeKey: input.scopeKey,
|
|
111
|
+
status: "draft",
|
|
112
|
+
version: input.version ?? 1,
|
|
113
|
+
artifact: null,
|
|
114
|
+
evidence: [],
|
|
115
|
+
exceptions: [],
|
|
116
|
+
checks: [],
|
|
117
|
+
provenance: input.provenance,
|
|
118
|
+
history: [],
|
|
119
|
+
createdAt: at,
|
|
120
|
+
updatedAt: at
|
|
121
|
+
},
|
|
122
|
+
input.extras
|
|
123
|
+
);
|
|
124
|
+
await appendEvent(record, "wp.created", `Work product draft v${record.version} created for ${record.scopeKey}`, {
|
|
125
|
+
scopeKey: record.scopeKey,
|
|
126
|
+
version: record.version,
|
|
127
|
+
threadId: record.threadId
|
|
128
|
+
});
|
|
129
|
+
return record;
|
|
130
|
+
};
|
|
131
|
+
async function findByScopeAndStatus(workspaceId, scopeKey, status) {
|
|
132
|
+
const rows = await store.listByWorkspace(workspaceId, { status: [status] });
|
|
133
|
+
return rows.find((row) => row.scopeKey === scopeKey) ?? null;
|
|
134
|
+
}
|
|
135
|
+
const nextVersion = async (workspaceId, scopeKey) => {
|
|
136
|
+
const reviewed = await store.listByWorkspace(workspaceId, { status: ["approved", "superseded"] });
|
|
137
|
+
const versions = reviewed.filter((row) => row.scopeKey === scopeKey).map((row) => row.version);
|
|
138
|
+
return versions.length === 0 ? 1 : Math.max(...versions) + 1;
|
|
139
|
+
};
|
|
140
|
+
const reopen = async (id) => {
|
|
141
|
+
const record = await store.load(id);
|
|
142
|
+
if (!record) return rejected(`Work product ${id} not found`);
|
|
143
|
+
if (record.status !== "changes_requested") {
|
|
144
|
+
return rejected(`Work product ${id} is ${record.status}; only changes_requested reopens`);
|
|
145
|
+
}
|
|
146
|
+
const updated = await store.update(
|
|
147
|
+
id,
|
|
148
|
+
{ status: "changes_requested", version: record.version },
|
|
149
|
+
{ status: "draft", version: record.version + 1, updatedAt: now() }
|
|
150
|
+
);
|
|
151
|
+
if (!updated) return lostRace(id);
|
|
152
|
+
await appendEvent(updated, "wp.reopened", `Correction draft v${updated.version} opened`, {
|
|
153
|
+
from: record.version,
|
|
154
|
+
to: updated.version
|
|
155
|
+
});
|
|
156
|
+
return { succeeded: true, value: updated };
|
|
157
|
+
};
|
|
158
|
+
const upsertEvidence = (id, entries) => guardedMerge(
|
|
159
|
+
id,
|
|
160
|
+
["draft", "blocked"],
|
|
161
|
+
(record) => ({ evidence: mergeById(record.evidence, entries) }),
|
|
162
|
+
{
|
|
163
|
+
step: "wp.evidence",
|
|
164
|
+
message: (record) => `Evidence upserted (${entries.length} entries, ${record.evidence.length} total)`,
|
|
165
|
+
metadata: () => ({ upserted: entries.map((entry) => entry.id) })
|
|
166
|
+
}
|
|
167
|
+
);
|
|
168
|
+
const upsertExceptions = async (id, entries) => {
|
|
169
|
+
const merged = await guardedMerge(
|
|
170
|
+
id,
|
|
171
|
+
["draft", "blocked"],
|
|
172
|
+
(record2) => ({ exceptions: mergeById(record2.exceptions, entries) }),
|
|
173
|
+
{
|
|
174
|
+
step: "wp.exception",
|
|
175
|
+
message: (record2) => `Exceptions upserted (${entries.length} entries, ${unresolvedBlockingExceptions(record2.exceptions).length} blocking unresolved)`,
|
|
176
|
+
metadata: () => ({ upserted: entries.map((entry) => entry.id) })
|
|
177
|
+
}
|
|
178
|
+
);
|
|
179
|
+
if (!merged.succeeded) return merged;
|
|
180
|
+
const record = merged.value;
|
|
181
|
+
const blocking = unresolvedBlockingExceptions(record.exceptions).length;
|
|
182
|
+
if (record.status === "draft" && blocking > 0) {
|
|
183
|
+
return transition(id, "blocked", {}, { blocking });
|
|
184
|
+
}
|
|
185
|
+
if (record.status === "blocked" && blocking === 0) {
|
|
186
|
+
return transition(id, "draft", {}, { blocking });
|
|
187
|
+
}
|
|
188
|
+
return merged;
|
|
189
|
+
};
|
|
190
|
+
const recordChecks = (id, checks) => guardedMerge(
|
|
191
|
+
id,
|
|
192
|
+
["draft", "blocked"],
|
|
193
|
+
() => ({ checks: checks.slice() }),
|
|
194
|
+
{
|
|
195
|
+
step: "wp.checks",
|
|
196
|
+
message: () => `Checks recorded (${checks.length}, ${checks.filter((check) => !check.passed).length} failed)`,
|
|
197
|
+
metadata: () => ({ failed: checks.filter((check) => !check.passed).map((check) => check.name) })
|
|
198
|
+
}
|
|
199
|
+
);
|
|
200
|
+
const submit = async (id, input) => {
|
|
201
|
+
const record = await store.load(id);
|
|
202
|
+
if (!record) return rejected(`Work product ${id} not found`);
|
|
203
|
+
if (record.status !== "draft") {
|
|
204
|
+
return rejected(`Work product ${id} is ${record.status}; only a draft submits`);
|
|
205
|
+
}
|
|
206
|
+
const blocking = unresolvedBlockingExceptions(record.exceptions);
|
|
207
|
+
if (blocking.length > 0) {
|
|
208
|
+
return rejected(
|
|
209
|
+
`Work product ${id} has ${blocking.length} unresolved blocking exception(s): ${blocking.map((entry2) => entry2.id).join(", ")}`
|
|
210
|
+
);
|
|
211
|
+
}
|
|
212
|
+
const artifactPath = input.artifactPath ?? input.artifact.path;
|
|
213
|
+
const entry = {
|
|
214
|
+
version: record.version,
|
|
215
|
+
status: "ready",
|
|
216
|
+
provenance: input.provenance,
|
|
217
|
+
...artifactPath === void 0 ? {} : { artifactPath },
|
|
218
|
+
at: now()
|
|
219
|
+
};
|
|
220
|
+
return transition(
|
|
221
|
+
id,
|
|
222
|
+
"ready",
|
|
223
|
+
{
|
|
224
|
+
artifact: input.artifact,
|
|
225
|
+
checks: input.checks.slice(),
|
|
226
|
+
provenance: input.provenance,
|
|
227
|
+
history: [...record.history, entry]
|
|
228
|
+
},
|
|
229
|
+
{ version: record.version, failedChecks: input.checks.filter((check) => !check.passed).length }
|
|
230
|
+
);
|
|
231
|
+
};
|
|
232
|
+
const applyVerdict = async (id, input) => {
|
|
233
|
+
const record = await store.load(id);
|
|
234
|
+
if (!record) return rejected(`Work product ${id} not found`);
|
|
235
|
+
if (record.status !== "ready") {
|
|
236
|
+
return rejected(`Work product ${id} is ${record.status}; a verdict applies only to ready`);
|
|
237
|
+
}
|
|
238
|
+
const to = input.verdict === "approve" ? "approved" : "changes_requested";
|
|
239
|
+
const entry = {
|
|
240
|
+
version: record.version,
|
|
241
|
+
status: to,
|
|
242
|
+
provenance: record.provenance,
|
|
243
|
+
...record.artifact?.path === void 0 ? {} : { artifactPath: record.artifact.path },
|
|
244
|
+
reviewedBy: input.reviewedBy,
|
|
245
|
+
...input.note === void 0 ? {} : { reviewNote: input.note },
|
|
246
|
+
at: now()
|
|
247
|
+
};
|
|
248
|
+
const outcome = await transition(
|
|
249
|
+
id,
|
|
250
|
+
to,
|
|
251
|
+
{ history: [...record.history, entry] },
|
|
252
|
+
{ verdict: input.verdict, reviewedBy: input.reviewedBy }
|
|
253
|
+
);
|
|
254
|
+
if (!outcome.succeeded || to !== "approved") return outcome;
|
|
255
|
+
const priorApproved = await store.listByWorkspace(record.workspaceId, { status: ["approved"] });
|
|
256
|
+
for (const prior of priorApproved) {
|
|
257
|
+
if (prior.id === id || prior.scopeKey !== record.scopeKey) continue;
|
|
258
|
+
await transition(prior.id, "superseded", {}, { supersededBy: id });
|
|
259
|
+
}
|
|
260
|
+
return outcome;
|
|
261
|
+
};
|
|
262
|
+
return {
|
|
263
|
+
create,
|
|
264
|
+
get: (id) => store.load(id),
|
|
265
|
+
openDraft: (workspaceId, scopeKey) => store.findDraft(workspaceId, scopeKey),
|
|
266
|
+
awaitingCorrection: (workspaceId, scopeKey) => findByScopeAndStatus(workspaceId, scopeKey, "changes_requested"),
|
|
267
|
+
awaitingReview: (workspaceId, scopeKey) => findByScopeAndStatus(workspaceId, scopeKey, "ready"),
|
|
268
|
+
nextVersion,
|
|
269
|
+
reopen,
|
|
270
|
+
upsertEvidence,
|
|
271
|
+
upsertExceptions,
|
|
272
|
+
recordChecks,
|
|
273
|
+
submit,
|
|
274
|
+
applyVerdict,
|
|
275
|
+
supersede: (id) => transition(id, "superseded")
|
|
276
|
+
};
|
|
277
|
+
}
|
|
278
|
+
function createInMemoryWorkProductStore() {
|
|
279
|
+
const rows = /* @__PURE__ */ new Map();
|
|
280
|
+
const events = [];
|
|
281
|
+
return {
|
|
282
|
+
async load(id) {
|
|
283
|
+
const record = rows.get(id);
|
|
284
|
+
return record ? structuredClone(record) : null;
|
|
285
|
+
},
|
|
286
|
+
async findDraft(workspaceId, scopeKey) {
|
|
287
|
+
for (const record of rows.values()) {
|
|
288
|
+
if (record.workspaceId === workspaceId && record.scopeKey === scopeKey && (record.status === "draft" || record.status === "blocked")) {
|
|
289
|
+
return structuredClone(record);
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
return null;
|
|
293
|
+
},
|
|
294
|
+
async listByWorkspace(workspaceId, opts) {
|
|
295
|
+
const out = [];
|
|
296
|
+
for (const record of rows.values()) {
|
|
297
|
+
if (record.workspaceId !== workspaceId) continue;
|
|
298
|
+
if (opts?.status && !opts.status.includes(record.status)) continue;
|
|
299
|
+
out.push(structuredClone(record));
|
|
300
|
+
}
|
|
301
|
+
return out;
|
|
302
|
+
},
|
|
303
|
+
async insert(record) {
|
|
304
|
+
if (rows.has(record.id)) throw new Error(`Work product ${record.id} already exists`);
|
|
305
|
+
rows.set(record.id, structuredClone(record));
|
|
306
|
+
return structuredClone(record);
|
|
307
|
+
},
|
|
308
|
+
async update(id, guard, patch) {
|
|
309
|
+
const current = rows.get(id);
|
|
310
|
+
if (!current) return null;
|
|
311
|
+
if (guard.status !== void 0 && current.status !== guard.status) return null;
|
|
312
|
+
if (guard.version !== void 0 && current.version !== guard.version) return null;
|
|
313
|
+
const next = { ...current };
|
|
314
|
+
if (patch.status !== void 0) next.status = patch.status;
|
|
315
|
+
if (patch.version !== void 0) next.version = patch.version;
|
|
316
|
+
if (patch.artifact !== void 0) next.artifact = patch.artifact;
|
|
317
|
+
if (patch.evidence !== void 0) next.evidence = patch.evidence;
|
|
318
|
+
if (patch.exceptions !== void 0) next.exceptions = patch.exceptions;
|
|
319
|
+
if (patch.checks !== void 0) next.checks = patch.checks;
|
|
320
|
+
if (patch.provenance !== void 0) next.provenance = patch.provenance;
|
|
321
|
+
if (patch.history !== void 0) next.history = patch.history;
|
|
322
|
+
if (patch.updatedAt !== void 0) next.updatedAt = patch.updatedAt;
|
|
323
|
+
rows.set(id, structuredClone(next));
|
|
324
|
+
return structuredClone(next);
|
|
325
|
+
},
|
|
326
|
+
async appendEvent(event) {
|
|
327
|
+
events.push(structuredClone(event));
|
|
328
|
+
},
|
|
329
|
+
events() {
|
|
330
|
+
return events.map((event) => structuredClone(event));
|
|
331
|
+
},
|
|
332
|
+
put(record) {
|
|
333
|
+
rows.set(record.id, structuredClone(record));
|
|
334
|
+
}
|
|
335
|
+
};
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
// src/work-product/provenance.ts
|
|
339
|
+
function stampProvenance(base, now = Date.now) {
|
|
340
|
+
return { ...base, servingModels: [], producedAt: now() };
|
|
341
|
+
}
|
|
342
|
+
async function finalizeWorkProductProvenance(store, input) {
|
|
343
|
+
const rows = await store.listByWorkspace(input.workspaceId);
|
|
344
|
+
const updated = [];
|
|
345
|
+
for (const record of rows) {
|
|
346
|
+
const recordMatches = record.provenance.runId === input.runId;
|
|
347
|
+
const historyMatches = record.history.some((entry) => entry.provenance.runId === input.runId);
|
|
348
|
+
if (!recordMatches && !historyMatches) continue;
|
|
349
|
+
const finalize = (provenance) => ({
|
|
350
|
+
...provenance,
|
|
351
|
+
servingModels: [...input.servingModels],
|
|
352
|
+
...input.costUsd === void 0 ? {} : { costUsd: input.costUsd }
|
|
353
|
+
});
|
|
354
|
+
const next = await store.update(
|
|
355
|
+
record.id,
|
|
356
|
+
{ status: record.status, version: record.version },
|
|
357
|
+
{
|
|
358
|
+
...recordMatches ? { provenance: finalize(record.provenance) } : {},
|
|
359
|
+
history: record.history.map(
|
|
360
|
+
(entry) => entry.provenance.runId === input.runId ? { ...entry, provenance: finalize(entry.provenance) } : entry
|
|
361
|
+
)
|
|
362
|
+
}
|
|
363
|
+
);
|
|
364
|
+
if (!next) {
|
|
365
|
+
input.logger?.warn(`[work-product] provenance back-fill lost a race on ${record.id}; skipped`);
|
|
366
|
+
continue;
|
|
367
|
+
}
|
|
368
|
+
await store.appendEvent({
|
|
369
|
+
workProductId: record.id,
|
|
370
|
+
workspaceId: record.workspaceId,
|
|
371
|
+
step: "wp.provenance",
|
|
372
|
+
message: `Serving models back-filled for run ${input.runId}`,
|
|
373
|
+
metadata: { runId: input.runId, servingModels: [...input.servingModels], costUsd: input.costUsd ?? null },
|
|
374
|
+
at: Date.now()
|
|
375
|
+
});
|
|
376
|
+
updated.push(next);
|
|
377
|
+
}
|
|
378
|
+
return updated;
|
|
379
|
+
}
|
|
380
|
+
function workProductTrustInputs(records, verdictsFor) {
|
|
381
|
+
const items = [];
|
|
382
|
+
for (const record of records) {
|
|
383
|
+
const verdicts = verdictsFor(record);
|
|
384
|
+
if (!verdicts || verdicts.length === 0) continue;
|
|
385
|
+
items.push({ itemId: record.id, verdicts });
|
|
386
|
+
}
|
|
387
|
+
return items;
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
// src/work-product/tools.ts
|
|
391
|
+
var MAX_WORK_PRODUCT_BATCH = 50;
|
|
392
|
+
var EVIDENCE_COVERAGE_CHECK = "evidence_coverage";
|
|
393
|
+
async function unwrap(run, code) {
|
|
394
|
+
let outcome = await run();
|
|
395
|
+
if (!outcome.succeeded && outcome.conflict) outcome = await run();
|
|
396
|
+
if (!outcome.succeeded) throw new ToolInputError(code, outcome.error, outcome.conflict ? 409 : 400);
|
|
397
|
+
return outcome.value;
|
|
398
|
+
}
|
|
399
|
+
function requireScopeKey(args) {
|
|
400
|
+
const scopeKey = typeof args.scopeKey === "string" ? args.scopeKey.trim() : "";
|
|
401
|
+
if (!scopeKey) throw new ToolInputError("missing_scope_key", "scopeKey is required \u2014 the engagement key this work product belongs to.");
|
|
402
|
+
return scopeKey;
|
|
403
|
+
}
|
|
404
|
+
function requireBatch(args, field) {
|
|
405
|
+
const raw = args[field];
|
|
406
|
+
if (!Array.isArray(raw) || raw.length === 0) {
|
|
407
|
+
throw new ToolInputError("missing_entries", `${field} must be a non-empty array.`);
|
|
408
|
+
}
|
|
409
|
+
if (raw.length > MAX_WORK_PRODUCT_BATCH) {
|
|
410
|
+
throw new ToolInputError("batch_too_large", `${field} accepts at most ${MAX_WORK_PRODUCT_BATCH} entries per call \u2014 send smaller batches.`);
|
|
411
|
+
}
|
|
412
|
+
return raw;
|
|
413
|
+
}
|
|
414
|
+
async function resolveDraft(service, config, scopeKey, ctx) {
|
|
415
|
+
const open = await service.openDraft(ctx.workspaceId, scopeKey);
|
|
416
|
+
if (open) return open;
|
|
417
|
+
const awaitingReview = await service.awaitingReview(ctx.workspaceId, scopeKey);
|
|
418
|
+
if (awaitingReview) {
|
|
419
|
+
throw new ToolInputError(
|
|
420
|
+
"awaiting_review",
|
|
421
|
+
`Work product for ${scopeKey} (v${awaitingReview.version}) is awaiting review \u2014 no further emission until a reviewer verdict.`,
|
|
422
|
+
409
|
|
423
|
+
);
|
|
424
|
+
}
|
|
425
|
+
const awaitingCorrection = await service.awaitingCorrection(ctx.workspaceId, scopeKey);
|
|
426
|
+
if (awaitingCorrection) {
|
|
427
|
+
return unwrap(() => service.reopen(awaitingCorrection.id), "reopen_failed");
|
|
428
|
+
}
|
|
429
|
+
return service.create({
|
|
430
|
+
workspaceId: ctx.workspaceId,
|
|
431
|
+
threadId: ctx.threadId,
|
|
432
|
+
scopeKey,
|
|
433
|
+
version: await service.nextVersion(ctx.workspaceId, scopeKey),
|
|
434
|
+
provenance: stampProvenance(config.provenance(ctx), config.now)
|
|
435
|
+
});
|
|
436
|
+
}
|
|
437
|
+
function buildWorkProductTools(config) {
|
|
438
|
+
const service = createWorkProductService({
|
|
439
|
+
store: config.store,
|
|
440
|
+
...config.now ? { now: config.now } : {},
|
|
441
|
+
...config.generateId ? { generateId: config.generateId } : {}
|
|
442
|
+
});
|
|
443
|
+
const upsertEvidence = defineAppTool({
|
|
444
|
+
name: "upsert_evidence",
|
|
445
|
+
description: "Record source\u2192field lineage for the current work product, incrementally as you find it. Each entry links a source document (sourceRef + locator) to one artifact target and states the claim it supports. Re-emitting an entry id replaces that entry. Address the work product by scopeKey; the first call creates the draft.",
|
|
446
|
+
parameters: {
|
|
447
|
+
type: "object",
|
|
448
|
+
properties: {
|
|
449
|
+
scopeKey: { type: "string", description: "Engagement key for this work product." },
|
|
450
|
+
entries: {
|
|
451
|
+
type: "array",
|
|
452
|
+
minItems: 1,
|
|
453
|
+
maxItems: MAX_WORK_PRODUCT_BATCH,
|
|
454
|
+
items: {
|
|
455
|
+
type: "object",
|
|
456
|
+
properties: {
|
|
457
|
+
id: { type: "string", description: "Stable entry id \u2014 re-emit to replace." },
|
|
458
|
+
sourceRef: { type: "string", description: "Vault path / attachment id of the SOURCE document." },
|
|
459
|
+
locator: {
|
|
460
|
+
type: "object",
|
|
461
|
+
properties: {
|
|
462
|
+
page: { type: "number" },
|
|
463
|
+
range: { type: "string", description: "Free-form location: 'L120-L134' | 'B7' | '\xB64'." },
|
|
464
|
+
quote: { type: "string", description: "Verbatim supporting quote." }
|
|
465
|
+
}
|
|
466
|
+
},
|
|
467
|
+
target: { type: "string", description: "Artifact field/claim this evidence supports." },
|
|
468
|
+
claim: { type: "string", description: "The value/assertion at the target." },
|
|
469
|
+
confidence: { type: "number", minimum: 0, maximum: 1 }
|
|
470
|
+
},
|
|
471
|
+
required: ["id", "sourceRef", "target", "claim"]
|
|
472
|
+
}
|
|
473
|
+
}
|
|
474
|
+
},
|
|
475
|
+
required: ["scopeKey", "entries"]
|
|
476
|
+
},
|
|
477
|
+
async execute(args, ctx) {
|
|
478
|
+
const scopeKey = requireScopeKey(args);
|
|
479
|
+
const raw = requireBatch(args, "entries");
|
|
480
|
+
const entries = [];
|
|
481
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
482
|
+
const parsed = parseEvidenceInput(raw[index], `entries[${index}]`);
|
|
483
|
+
if (!parsed.ok) throw new ToolInputError("invalid_evidence", `${parsed.field}: ${parsed.error}`);
|
|
484
|
+
entries.push(parsed.value);
|
|
485
|
+
}
|
|
486
|
+
for (let index = 0; index < entries.length; index += 1) {
|
|
487
|
+
const entry = entries[index];
|
|
488
|
+
if (!await config.resolveSourceRef(entry.sourceRef, ctx)) {
|
|
489
|
+
throw new ToolInputError(
|
|
490
|
+
"unknown_source_ref",
|
|
491
|
+
`entries[${index}].sourceRef: "${entry.sourceRef}" does not resolve to an existing source document.`
|
|
492
|
+
);
|
|
493
|
+
}
|
|
494
|
+
}
|
|
495
|
+
const draft = await resolveDraft(service, config, scopeKey, ctx);
|
|
496
|
+
const record = await unwrap(() => service.upsertEvidence(draft.id, entries), "evidence_rejected");
|
|
497
|
+
return { workProductId: record.id, version: record.version, evidenceCount: record.evidence.length };
|
|
498
|
+
}
|
|
499
|
+
});
|
|
500
|
+
const flagException = defineAppTool({
|
|
501
|
+
name: "flag_exception",
|
|
502
|
+
description: "Flag problems with the current work product (missing documents, inconsistent sources, \u2026). An unresolved blocking exception parks the work product until it is resolved; re-emit the same id with resolved:true to release it. Address by scopeKey.",
|
|
503
|
+
parameters: {
|
|
504
|
+
type: "object",
|
|
505
|
+
properties: {
|
|
506
|
+
scopeKey: { type: "string", description: "Engagement key for this work product." },
|
|
507
|
+
exceptions: {
|
|
508
|
+
type: "array",
|
|
509
|
+
minItems: 1,
|
|
510
|
+
maxItems: MAX_WORK_PRODUCT_BATCH,
|
|
511
|
+
items: {
|
|
512
|
+
type: "object",
|
|
513
|
+
properties: {
|
|
514
|
+
id: { type: "string", description: "Stable entry id \u2014 re-emit to replace." },
|
|
515
|
+
severity: { type: "string", enum: ["blocking", "material", "advisory"] },
|
|
516
|
+
kind: { type: "string", description: "Exception kind from the product vocabulary." },
|
|
517
|
+
message: { type: "string" },
|
|
518
|
+
targets: { type: "array", items: { type: "string" } },
|
|
519
|
+
resolved: { type: "boolean" },
|
|
520
|
+
resolutionNote: { type: "string" }
|
|
521
|
+
},
|
|
522
|
+
required: ["id", "severity", "kind", "message"]
|
|
523
|
+
}
|
|
524
|
+
}
|
|
525
|
+
},
|
|
526
|
+
required: ["scopeKey", "exceptions"]
|
|
527
|
+
},
|
|
528
|
+
async execute(args, ctx) {
|
|
529
|
+
const scopeKey = requireScopeKey(args);
|
|
530
|
+
const raw = requireBatch(args, "exceptions");
|
|
531
|
+
const entries = [];
|
|
532
|
+
for (let index = 0; index < raw.length; index += 1) {
|
|
533
|
+
const parsed = parseExceptionInput(raw[index], `exceptions[${index}]`);
|
|
534
|
+
if (!parsed.ok) throw new ToolInputError("invalid_exception", `${parsed.field}: ${parsed.error}`);
|
|
535
|
+
if (!config.exceptionKinds.includes(parsed.value.kind)) {
|
|
536
|
+
throw new ToolInputError(
|
|
537
|
+
"invalid_exception",
|
|
538
|
+
`exceptions[${index}].kind: must be one of: ${config.exceptionKinds.join(", ")}`
|
|
539
|
+
);
|
|
540
|
+
}
|
|
541
|
+
if (parsed.value.resolved && parsed.value.resolvedBy === void 0) parsed.value.resolvedBy = "agent";
|
|
542
|
+
entries.push(parsed.value);
|
|
543
|
+
}
|
|
544
|
+
const draft = await resolveDraft(service, config, scopeKey, ctx);
|
|
545
|
+
const record = await unwrap(() => service.upsertExceptions(draft.id, entries), "exception_rejected");
|
|
546
|
+
return {
|
|
547
|
+
workProductId: record.id,
|
|
548
|
+
version: record.version,
|
|
549
|
+
status: record.status,
|
|
550
|
+
unresolvedBlocking: unresolvedBlockingExceptions(record.exceptions).length
|
|
551
|
+
};
|
|
552
|
+
}
|
|
553
|
+
});
|
|
554
|
+
const submitWorkProduct = defineAppTool({
|
|
555
|
+
name: "submit_work_product",
|
|
556
|
+
description: "Submit the finished work product for professional review \u2014 the terminal call after evidence and exceptions are recorded. Refused while a blocking exception is unresolved, or when a material target lacks evidence. Include your own quality checks in `checks`.",
|
|
557
|
+
parameters: {
|
|
558
|
+
type: "object",
|
|
559
|
+
properties: {
|
|
560
|
+
scopeKey: { type: "string", description: "Engagement key for this work product." },
|
|
561
|
+
artifact: {
|
|
562
|
+
type: "object",
|
|
563
|
+
properties: {
|
|
564
|
+
kind: { type: "string", description: "Artifact kind from the product vocabulary." },
|
|
565
|
+
title: { type: "string" },
|
|
566
|
+
path: { type: "string", description: "Vault/object-store ref of the rendered document." },
|
|
567
|
+
content: { type: "string", description: "Inline body when small (markdown/JSON)." },
|
|
568
|
+
mediaType: { type: "string" },
|
|
569
|
+
baseline: {
|
|
570
|
+
type: "object",
|
|
571
|
+
properties: { path: { type: "string" }, content: { type: "string" } },
|
|
572
|
+
description: "For diff-first artifacts: the source document being redlined."
|
|
573
|
+
},
|
|
574
|
+
fields: { type: "object", description: "Structured field map lineage targets anchor to." }
|
|
575
|
+
},
|
|
576
|
+
required: ["kind", "title"]
|
|
577
|
+
},
|
|
578
|
+
checks: {
|
|
579
|
+
type: "array",
|
|
580
|
+
items: {
|
|
581
|
+
type: "object",
|
|
582
|
+
properties: {
|
|
583
|
+
id: { type: "string" },
|
|
584
|
+
name: { type: "string" },
|
|
585
|
+
passed: { type: "boolean" },
|
|
586
|
+
detail: { type: "string" }
|
|
587
|
+
},
|
|
588
|
+
required: ["id", "name", "passed"]
|
|
589
|
+
},
|
|
590
|
+
description: "Your own quality self-checks (recorded as agent-sourced)."
|
|
591
|
+
}
|
|
592
|
+
},
|
|
593
|
+
required: ["scopeKey", "artifact"]
|
|
594
|
+
},
|
|
595
|
+
async execute(args, ctx) {
|
|
596
|
+
const scopeKey = requireScopeKey(args);
|
|
597
|
+
const parsedArtifact = parseArtifactInput(args.artifact);
|
|
598
|
+
if (!parsedArtifact.ok) throw new ToolInputError("invalid_artifact", `${parsedArtifact.field}: ${parsedArtifact.error}`);
|
|
599
|
+
const artifact = parsedArtifact.value;
|
|
600
|
+
if (!config.artifactKinds.includes(artifact.kind)) {
|
|
601
|
+
throw new ToolInputError("invalid_artifact", `artifact.kind: must be one of: ${config.artifactKinds.join(", ")}`);
|
|
602
|
+
}
|
|
603
|
+
const agentChecks = [];
|
|
604
|
+
if (args.checks !== void 0) {
|
|
605
|
+
if (!Array.isArray(args.checks)) throw new ToolInputError("invalid_checks", "checks must be an array when present.");
|
|
606
|
+
for (let index = 0; index < args.checks.length; index += 1) {
|
|
607
|
+
const parsed = parseAgentCheckInput(args.checks[index], `checks[${index}]`);
|
|
608
|
+
if (!parsed.ok) throw new ToolInputError("invalid_checks", `${parsed.field}: ${parsed.error}`);
|
|
609
|
+
agentChecks.push({ ...parsed.value, source: "agent" });
|
|
610
|
+
}
|
|
611
|
+
}
|
|
612
|
+
const draft = await resolveDraft(service, config, scopeKey, ctx);
|
|
613
|
+
const blocking = unresolvedBlockingExceptions(draft.exceptions);
|
|
614
|
+
if (blocking.length > 0) {
|
|
615
|
+
throw new ToolInputError(
|
|
616
|
+
"blocking_exceptions_unresolved",
|
|
617
|
+
`Cannot submit: ${blocking.length} unresolved blocking exception(s) (${blocking.map((entry) => entry.id).join(", ")}). Resolve each via flag_exception with resolved:true, or downgrade its severity with a resolutionNote justifying why it does not block.`,
|
|
618
|
+
409
|
|
619
|
+
);
|
|
620
|
+
}
|
|
621
|
+
const checks = [...agentChecks];
|
|
622
|
+
if (config.materialTargets) {
|
|
623
|
+
const targets = config.materialTargets(artifact);
|
|
624
|
+
const covered = new Set(draft.evidence.map((entry) => entry.target));
|
|
625
|
+
const missing = targets.filter((target) => !covered.has(target));
|
|
626
|
+
const coverage = {
|
|
627
|
+
id: EVIDENCE_COVERAGE_CHECK,
|
|
628
|
+
name: EVIDENCE_COVERAGE_CHECK,
|
|
629
|
+
passed: missing.length === 0,
|
|
630
|
+
detail: missing.length === 0 ? `${targets.length}/${targets.length} material targets evidenced` : `Missing evidence for: ${missing.join(", ")}`,
|
|
631
|
+
source: "platform"
|
|
632
|
+
};
|
|
633
|
+
checks.unshift(coverage);
|
|
634
|
+
if (missing.length > 0) {
|
|
635
|
+
await unwrap(() => service.recordChecks(draft.id, checks), "checks_rejected");
|
|
636
|
+
throw new ToolInputError(
|
|
637
|
+
"evidence_coverage_failed",
|
|
638
|
+
`Cannot submit: material targets lack evidence: ${missing.join(", ")}. Add upsert_evidence entries targeting each, then resubmit.`
|
|
639
|
+
);
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
const provenance = stampProvenance(config.provenance(ctx), config.now);
|
|
643
|
+
const record = await unwrap(
|
|
644
|
+
() => service.submit(draft.id, { artifact, checks, provenance }),
|
|
645
|
+
"submit_rejected"
|
|
646
|
+
);
|
|
647
|
+
await config.onReady?.(record, ctx);
|
|
648
|
+
return {
|
|
649
|
+
workProductId: record.id,
|
|
650
|
+
version: record.version,
|
|
651
|
+
status: record.status,
|
|
652
|
+
checks: record.checks.map((check) => ({ name: check.name, passed: check.passed, source: check.source }))
|
|
653
|
+
};
|
|
654
|
+
}
|
|
655
|
+
});
|
|
656
|
+
return [upsertEvidence, flagException, submitWorkProduct];
|
|
657
|
+
}
|
|
658
|
+
|
|
659
|
+
// src/work-product/route.ts
|
|
660
|
+
function validateWorkProductVerdictBody(body) {
|
|
661
|
+
const id = typeof body.id === "string" && body.id.trim() ? body.id.trim() : null;
|
|
662
|
+
if (!id) return { ok: false, error: "Missing work product id" };
|
|
663
|
+
const verdict = body.verdict;
|
|
664
|
+
if (verdict !== "approve" && verdict !== "request_changes") {
|
|
665
|
+
return { ok: false, error: "Invalid verdict: expected approve or request_changes" };
|
|
666
|
+
}
|
|
667
|
+
const note = body.note === void 0 ? void 0 : typeof body.note === "string" ? body.note.trim() : null;
|
|
668
|
+
if (note === null) return { ok: false, error: "Invalid note: expected a string" };
|
|
669
|
+
if (verdict === "request_changes") {
|
|
670
|
+
if (!note) return { ok: false, error: "request_changes requires a note \u2014 it becomes the correction instruction in chat" };
|
|
671
|
+
return { ok: true, id, verdict, note };
|
|
672
|
+
}
|
|
673
|
+
return note ? { ok: true, id, verdict, note } : { ok: true, id, verdict };
|
|
674
|
+
}
|
|
675
|
+
function createWorkProductRoutes(options) {
|
|
676
|
+
const logger = options.logger ?? console;
|
|
677
|
+
const service = createWorkProductService({
|
|
678
|
+
store: options.store,
|
|
679
|
+
...options.now ? { now: options.now } : {}
|
|
680
|
+
});
|
|
681
|
+
async function list(request) {
|
|
682
|
+
const auth = await options.authorize({ request, intent: "list" });
|
|
683
|
+
if (!auth.ok) return auth.response;
|
|
684
|
+
const url = new URL(request.url);
|
|
685
|
+
const statusParam = url.searchParams.get("status");
|
|
686
|
+
const statuses = statusParam ? statusParam.split(",").map((value) => value.trim()).filter(Boolean) : null;
|
|
687
|
+
const workProducts = await options.store.listByWorkspace(
|
|
688
|
+
auth.workspaceId,
|
|
689
|
+
statuses ? { status: statuses } : void 0
|
|
690
|
+
);
|
|
691
|
+
return Response.json({ workProducts });
|
|
692
|
+
}
|
|
693
|
+
async function detail(request, id) {
|
|
694
|
+
const auth = await options.authorize({ request, intent: "detail" });
|
|
695
|
+
if (!auth.ok) return auth.response;
|
|
696
|
+
const record = await options.store.load(id);
|
|
697
|
+
if (!record || record.workspaceId !== auth.workspaceId) {
|
|
698
|
+
return Response.json({ error: "Work product not found" }, { status: 404 });
|
|
699
|
+
}
|
|
700
|
+
return Response.json({ workProduct: record });
|
|
701
|
+
}
|
|
702
|
+
async function verdict(request) {
|
|
703
|
+
const body = await request.json().catch(() => null);
|
|
704
|
+
if (!body || typeof body !== "object" || Array.isArray(body)) {
|
|
705
|
+
return Response.json({ error: "Invalid JSON body" }, { status: 400 });
|
|
706
|
+
}
|
|
707
|
+
const validation = validateWorkProductVerdictBody(body);
|
|
708
|
+
if (!validation.ok) return Response.json({ error: validation.error }, { status: 400 });
|
|
709
|
+
const auth = await options.authorize({ request, intent: "verdict", body });
|
|
710
|
+
if (!auth.ok) return auth.response;
|
|
711
|
+
const existing = await options.store.load(validation.id);
|
|
712
|
+
if (!existing || existing.workspaceId !== auth.workspaceId) {
|
|
713
|
+
return Response.json({ error: "Work product not found" }, { status: 404 });
|
|
714
|
+
}
|
|
715
|
+
const outcome = await service.applyVerdict(validation.id, {
|
|
716
|
+
verdict: validation.verdict,
|
|
717
|
+
reviewedBy: auth.reviewedBy,
|
|
718
|
+
...validation.note === void 0 ? {} : { note: validation.note }
|
|
719
|
+
});
|
|
720
|
+
if (!outcome.succeeded) {
|
|
721
|
+
return Response.json({ code: "VERDICT_CONFLICT", error: outcome.error }, { status: 409 });
|
|
722
|
+
}
|
|
723
|
+
const record = outcome.value;
|
|
724
|
+
try {
|
|
725
|
+
await options.persistAnchorPart?.(workProductToPersistedPart(record), record);
|
|
726
|
+
} catch (error) {
|
|
727
|
+
logger.error("[work-product] persistAnchorPart failed:", error);
|
|
728
|
+
}
|
|
729
|
+
try {
|
|
730
|
+
await options.onVerdict?.({
|
|
731
|
+
record,
|
|
732
|
+
verdict: validation.verdict,
|
|
733
|
+
...validation.note === void 0 ? {} : { note: validation.note },
|
|
734
|
+
reviewedBy: auth.reviewedBy
|
|
735
|
+
});
|
|
736
|
+
} catch (error) {
|
|
737
|
+
logger.error("[work-product] onVerdict failed:", error);
|
|
738
|
+
}
|
|
739
|
+
if (validation.verdict === "approve") {
|
|
740
|
+
try {
|
|
741
|
+
await options.onExport?.(record);
|
|
742
|
+
} catch (error) {
|
|
743
|
+
logger.error("[work-product] onExport failed:", error);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
return Response.json({ ok: true, workProduct: record });
|
|
747
|
+
}
|
|
748
|
+
return { list, detail, verdict };
|
|
749
|
+
}
|
|
30
750
|
export {
|
|
31
751
|
EVIDENCE_COVERAGE_CHECK,
|
|
32
752
|
MAX_WORK_PRODUCT_BATCH,
|