@fieldwangai/agentflow 0.1.135 → 0.1.137
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/bin/lib/prd-workflow-collaboration.mjs +172 -17
- package/bin/lib/ui-server.mjs +869 -80
- package/bin/lib/workflow-report.mjs +357 -36
- package/builtin/web-ui/dist/assets/index-CQsrSc3u.css +1 -0
- package/builtin/web-ui/dist/assets/index-DQvqqAeQ.js +590 -0
- package/builtin/web-ui/dist/index.html +2 -2
- package/package.json +1 -1
- package/skills/agentflow-cli/SKILL.md +3 -1
- package/skills/agentflow-cli/scripts/agentflow-cli.mjs +55 -6
- package/skills/agentflow-cli/scripts/workflow-report-client.mjs +71 -0
- package/skills/agentflow-workflow-report/SKILL.md +40 -17
- package/skills/agentflow-workflow-report/references/protocol.md +529 -235
- package/builtin/web-ui/dist/assets/index-BFQVTav-.css +0 -1
- package/builtin/web-ui/dist/assets/index-CmpbCHAj.js +0 -420
package/bin/lib/ui-server.mjs
CHANGED
|
@@ -156,6 +156,7 @@ import {
|
|
|
156
156
|
ensurePrdWorkflowCollaboration,
|
|
157
157
|
getPrdWorkflowCollaborationById,
|
|
158
158
|
getPrdWorkflowCollaborationByShareToken,
|
|
159
|
+
getPrdWorkflowCollaborationByTapdId,
|
|
159
160
|
getPrdWorkflowCollaborationForUser,
|
|
160
161
|
ensurePrdWorkflowShareLink,
|
|
161
162
|
listPrdWorkflowCollaborationsForUser,
|
|
@@ -164,6 +165,7 @@ import {
|
|
|
164
165
|
prdWorkflowCollaborationSummary,
|
|
165
166
|
removePrdWorkflowCollaborationMember,
|
|
166
167
|
revokePrdWorkflowShareLink,
|
|
168
|
+
syncPrdWorkflowAuthority,
|
|
167
169
|
} from "./prd-workflow-collaboration.mjs";
|
|
168
170
|
import {
|
|
169
171
|
createTeam,
|
|
@@ -176,6 +178,7 @@ import {
|
|
|
176
178
|
} from "./teams.mjs";
|
|
177
179
|
import {
|
|
178
180
|
legacyOverallToGlobalState,
|
|
181
|
+
materializeWorkflowExtensions,
|
|
179
182
|
materializeWorkflowGlobalState,
|
|
180
183
|
materializeWorkflowProjections,
|
|
181
184
|
mergeWorkflowArtifactLists,
|
|
@@ -183,7 +186,10 @@ import {
|
|
|
183
186
|
mergeWorkflowGlobalState,
|
|
184
187
|
normalizeWorkflowReference,
|
|
185
188
|
normalizeWorkflowReport,
|
|
189
|
+
removeWorkflowGlobalStatePath,
|
|
190
|
+
workflowReportResourceKeys,
|
|
186
191
|
workflowRuntimeRevision,
|
|
192
|
+
workflowSnapshotResourceVersions,
|
|
187
193
|
} from "./workflow-report.mjs";
|
|
188
194
|
|
|
189
195
|
const MIME = {
|
|
@@ -1445,11 +1451,31 @@ function skillhubInstallArgs(payload, { uninstall = false } = {}) {
|
|
|
1445
1451
|
return args;
|
|
1446
1452
|
}
|
|
1447
1453
|
|
|
1448
|
-
function readBody(req) {
|
|
1454
|
+
function readBody(req, maxBytes = 5 * 1024 * 1024) {
|
|
1449
1455
|
return new Promise((resolve, reject) => {
|
|
1450
1456
|
const chunks = [];
|
|
1451
|
-
|
|
1452
|
-
|
|
1457
|
+
let total = 0;
|
|
1458
|
+
let exceeded = false;
|
|
1459
|
+
req.on("data", (chunk) => {
|
|
1460
|
+
if (exceeded) return;
|
|
1461
|
+
const value = Buffer.from(chunk);
|
|
1462
|
+
total += value.length;
|
|
1463
|
+
if (total > maxBytes) {
|
|
1464
|
+
exceeded = true;
|
|
1465
|
+
chunks.length = 0;
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
chunks.push(value);
|
|
1469
|
+
});
|
|
1470
|
+
req.on("end", () => {
|
|
1471
|
+
if (exceeded) {
|
|
1472
|
+
const error = new Error(`Request body exceeds ${maxBytes} bytes`);
|
|
1473
|
+
error.status = 413;
|
|
1474
|
+
reject(error);
|
|
1475
|
+
return;
|
|
1476
|
+
}
|
|
1477
|
+
resolve(Buffer.concat(chunks).toString("utf8"));
|
|
1478
|
+
});
|
|
1453
1479
|
req.on("error", reject);
|
|
1454
1480
|
});
|
|
1455
1481
|
}
|
|
@@ -3314,6 +3340,21 @@ function findWorkspaceShareUser(username) {
|
|
|
3314
3340
|
return null;
|
|
3315
3341
|
}
|
|
3316
3342
|
|
|
3343
|
+
function workflowAuthorityIdentity(value) {
|
|
3344
|
+
if (typeof value === "string" || typeof value === "number") return String(value || "").trim();
|
|
3345
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return "";
|
|
3346
|
+
return String(value.username || value.userId || value.user_id || value.nick || value.name || "").trim();
|
|
3347
|
+
}
|
|
3348
|
+
|
|
3349
|
+
function workflowAuthorityIdentities(value) {
|
|
3350
|
+
const values = Array.isArray(value) ? value : value == null ? [] : [value];
|
|
3351
|
+
return [...new Set(values.flatMap((item) => {
|
|
3352
|
+
if (typeof item === "string") return item.split(/[;,,;]/).map((entry) => entry.trim()).filter(Boolean);
|
|
3353
|
+
const identity = workflowAuthorityIdentity(item);
|
|
3354
|
+
return identity ? [identity] : [];
|
|
3355
|
+
}))];
|
|
3356
|
+
}
|
|
3357
|
+
|
|
3317
3358
|
function workspaceCollaborationSummaryWithUsers(record, userId) {
|
|
3318
3359
|
const summary = workspaceCollaborationSummary(record, userId);
|
|
3319
3360
|
if (!summary) return null;
|
|
@@ -7779,6 +7820,7 @@ const workspaceCollaborationSequences = new Map();
|
|
|
7779
7820
|
const prdWorkflowSubscribers = new Map();
|
|
7780
7821
|
const prdWorkflowIdempotency = new Map();
|
|
7781
7822
|
const prdWorkflowActionLocks = new Map();
|
|
7823
|
+
const prdWorkflowWriteQueues = new Map();
|
|
7782
7824
|
const PRD_WORKFLOW_IDEMPOTENCY_MAX = 1000;
|
|
7783
7825
|
const PRD_WORKFLOW_RUNTIME_EVENTS_MAX = 1000;
|
|
7784
7826
|
const WORKSPACE_SCHEDULES_FILENAME = "workspace-schedules.json";
|
|
@@ -7787,6 +7829,22 @@ const WORKSPACE_IMPLEMENTATION_REFERENCE_ENABLED = true;
|
|
|
7787
7829
|
const WORKSPACE_IMPLEMENTATION_SUMMARY_ENABLED = false;
|
|
7788
7830
|
const WORKSPACE_NODE_HISTORY_MAX_CHARS = 80000;
|
|
7789
7831
|
|
|
7832
|
+
async function prdWorkflowAcquireWriteLock(key) {
|
|
7833
|
+
const lockKey = String(key || "").trim();
|
|
7834
|
+
const previous = prdWorkflowWriteQueues.get(lockKey) || Promise.resolve();
|
|
7835
|
+
let releaseCurrent;
|
|
7836
|
+
const current = new Promise((resolve) => { releaseCurrent = resolve; });
|
|
7837
|
+
prdWorkflowWriteQueues.set(lockKey, current);
|
|
7838
|
+
await previous.catch(() => {});
|
|
7839
|
+
let released = false;
|
|
7840
|
+
return () => {
|
|
7841
|
+
if (released) return;
|
|
7842
|
+
released = true;
|
|
7843
|
+
releaseCurrent();
|
|
7844
|
+
if (prdWorkflowWriteQueues.get(lockKey) === current) prdWorkflowWriteQueues.delete(lockKey);
|
|
7845
|
+
};
|
|
7846
|
+
}
|
|
7847
|
+
|
|
7790
7848
|
function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capability = "read") {
|
|
7791
7849
|
const tapdId = String(params.tapdId || params.tapd_id || "").trim();
|
|
7792
7850
|
const flowId = String(params.flowId || "").trim();
|
|
@@ -7811,6 +7869,10 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
7811
7869
|
const memberCollaboration = tapdId
|
|
7812
7870
|
? getPrdWorkflowCollaborationForUser(tapdId, userCtx?.userId)
|
|
7813
7871
|
: null;
|
|
7872
|
+
const existingCollaboration = tapdId ? getPrdWorkflowCollaborationByTapdId(tapdId) : null;
|
|
7873
|
+
if (!adminOwner && !linkCollaboration && existingCollaboration && !memberCollaboration) {
|
|
7874
|
+
return { error: "PRD Workflow collaboration permission denied", status: 403 };
|
|
7875
|
+
}
|
|
7814
7876
|
const collaboration = adminOwner ? null : (linkCollaboration || memberCollaboration);
|
|
7815
7877
|
const access = adminOwner
|
|
7816
7878
|
? { allowed: true, writable: false, role: "admin-viewer", via: "admin-review" }
|
|
@@ -7824,7 +7886,8 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
7824
7886
|
return { error: "PRD Workflow collaboration edit permission denied", status: 403 };
|
|
7825
7887
|
}
|
|
7826
7888
|
const ownerId = String(adminOwner?.userId || collaboration?.ownerId || userCtx?.userId || "").trim();
|
|
7827
|
-
const
|
|
7889
|
+
const stateOwnerId = String(adminOwner?.userId || collaboration?.stateOwnerId || collaboration?.ownerId || userCtx?.userId || "").trim();
|
|
7890
|
+
const stateRoot = path.resolve(getAgentflowUserDataRoot(stateOwnerId));
|
|
7828
7891
|
let executionRoot = path.resolve(workspaceRoot);
|
|
7829
7892
|
if (flowId) {
|
|
7830
7893
|
const projectScope = resolveWorkspaceScopeRoot(workspaceRoot, {
|
|
@@ -7846,6 +7909,7 @@ function resolvePrdWorkflowScope(workspaceRoot, params = {}, userCtx = {}, capab
|
|
|
7846
7909
|
executionRoot,
|
|
7847
7910
|
stateRoot,
|
|
7848
7911
|
ownerId,
|
|
7912
|
+
stateOwnerId,
|
|
7849
7913
|
collaboration,
|
|
7850
7914
|
collaborationAccess: access,
|
|
7851
7915
|
shareToken,
|
|
@@ -7862,7 +7926,7 @@ function prdWorkflowKey(userCtx = {}, flowSource = "user", flowId = "", tapdId =
|
|
|
7862
7926
|
const collaboration = getPrdWorkflowCollaborationByShareToken(shareToken)
|
|
7863
7927
|
|| getPrdWorkflowCollaborationForUser(id, userCtx?.userId);
|
|
7864
7928
|
const adminOwnerId = userCtx?.isAdmin === true ? String(userCtx?.adminOwnerId || "").trim() : "";
|
|
7865
|
-
const actorScope = `user:${String(collaboration?.ownerId || adminOwnerId || userCtx?.userId || "")}`;
|
|
7929
|
+
const actorScope = `user:${String(collaboration?.stateOwnerId || collaboration?.ownerId || adminOwnerId || userCtx?.userId || "")}`;
|
|
7866
7930
|
return [actorScope, id].join("\t");
|
|
7867
7931
|
}
|
|
7868
7932
|
|
|
@@ -7997,7 +8061,17 @@ function prdWorkflowSafeStateId(value) {
|
|
|
7997
8061
|
}
|
|
7998
8062
|
|
|
7999
8063
|
function prdWorkflowReviewIdFromRequest(tapdId, payload = {}, durability = "temporary") {
|
|
8000
|
-
if (durability === "temporary")
|
|
8064
|
+
if (durability === "temporary") {
|
|
8065
|
+
const idempotencyKey = String(payload.idempotencyKey || payload.idempotency_key || "").trim();
|
|
8066
|
+
if (!idempotencyKey) return `r-${crypto.randomBytes(5).toString("hex")}`;
|
|
8067
|
+
const source = String(payload.source || "agentflow-cli").trim().toLowerCase() || "agentflow-cli";
|
|
8068
|
+
const digest = crypto
|
|
8069
|
+
.createHash("sha256")
|
|
8070
|
+
.update(JSON.stringify({ tapdId: String(tapdId || ""), source, idempotencyKey }))
|
|
8071
|
+
.digest("hex")
|
|
8072
|
+
.slice(0, 12);
|
|
8073
|
+
return `r-${digest}`;
|
|
8074
|
+
}
|
|
8001
8075
|
const requested = String(payload.reviewId || payload.review_id || "").trim();
|
|
8002
8076
|
if (!requested) return `review_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
|
|
8003
8077
|
const safeRequested = prdWorkflowSafeStateId(requested);
|
|
@@ -8064,6 +8138,11 @@ function prdWorkflowEventsPath(scopedRoot, tapdId) {
|
|
|
8064
8138
|
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.events.json`);
|
|
8065
8139
|
}
|
|
8066
8140
|
|
|
8141
|
+
function prdWorkflowEventsArchivePath(scopedRoot, tapdId) {
|
|
8142
|
+
const rootDir = scopedRoot || process.cwd();
|
|
8143
|
+
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.events.archive.jsonl`);
|
|
8144
|
+
}
|
|
8145
|
+
|
|
8067
8146
|
function prdWorkflowAuditPath(scopedRoot, tapdId) {
|
|
8068
8147
|
const rootDir = scopedRoot || process.cwd();
|
|
8069
8148
|
return path.join(rootDir, ".workspace", "prd-flow", "workflow-state", `${prdWorkflowSafeStateId(tapdId)}.audit.jsonl`);
|
|
@@ -9701,10 +9780,20 @@ export function prdWorkflowReviewHtml(title, markdown, meta = {}) {
|
|
|
9701
9780
|
}
|
|
9702
9781
|
|
|
9703
9782
|
function prdWorkflowCreateReview(scopedRoot, tapdId, payload = {}, urlBase = "", ownerId = "") {
|
|
9704
|
-
const content = String(payload.markdown || payload.content || payload.rawOutput || "")
|
|
9783
|
+
const content = String(payload.markdown || payload.content || payload.rawOutput || "");
|
|
9705
9784
|
if (!content.trim()) throw new Error("Missing review markdown");
|
|
9785
|
+
if (Buffer.byteLength(content, "utf-8") > 500000) {
|
|
9786
|
+
const error = new Error("Review markdown exceeds 500000 bytes");
|
|
9787
|
+
error.status = 413;
|
|
9788
|
+
throw error;
|
|
9789
|
+
}
|
|
9706
9790
|
const title = String(payload.title || payload.label || "PRD Workflow Review").trim().slice(0, 160) || "PRD Workflow Review";
|
|
9707
9791
|
const durability = String(payload.durability || (payload.durable === true || payload.permanent === true ? "durable" : "temporary")).trim().toLowerCase() || "temporary";
|
|
9792
|
+
if (!["temporary", "durable"].includes(durability)) {
|
|
9793
|
+
const error = new Error("durability must be temporary or durable");
|
|
9794
|
+
error.status = 400;
|
|
9795
|
+
throw error;
|
|
9796
|
+
}
|
|
9708
9797
|
const reviewId = prdWorkflowReviewIdFromRequest(tapdId, payload, durability);
|
|
9709
9798
|
const paths = prdWorkflowReviewPaths(scopedRoot, tapdId, reviewId);
|
|
9710
9799
|
const ttlDaysRaw = Number(payload.ttlDays || payload.ttl_days || (durability === "temporary" ? 7 : 0));
|
|
@@ -9924,11 +10013,13 @@ function prdWorkflowReadClientStateWithFallback(root, scopedRoot, tapdId) {
|
|
|
9924
10013
|
|
|
9925
10014
|
function prdWorkflowWriteClientObservation(scopedRoot, tapdId, meta, snapshot) {
|
|
9926
10015
|
const state = prdWorkflowReadClientState(scopedRoot, tapdId);
|
|
9927
|
-
const
|
|
10016
|
+
const reportSource = String(meta.reportSource || meta.source || "legacy").trim().toLowerCase() || "legacy";
|
|
10017
|
+
const clientId = prdWorkflowSafeStateId(`${reportSource}:${meta.clientId || "anonymous"}`);
|
|
9928
10018
|
const nextClients = {
|
|
9929
10019
|
...state.clients,
|
|
9930
10020
|
[clientId]: {
|
|
9931
10021
|
clientId: String(meta.clientId || clientId),
|
|
10022
|
+
source: reportSource,
|
|
9932
10023
|
userId: String(meta.userId || ""),
|
|
9933
10024
|
observedAt: String(meta.observedAt || ""),
|
|
9934
10025
|
reportedAt: String(meta.reportedAt || new Date().toISOString()),
|
|
@@ -10161,6 +10252,7 @@ function prdWorkflowSnapshotMetaFromReport(payload = {}, rawSnapshot = {}, req =
|
|
|
10161
10252
|
reportedAt,
|
|
10162
10253
|
observedAt: String(payload.observedAt || payload.observed_at || rawSnapshot.observedAt || rawSnapshot.observed_at || sources.observedAt || sources.checkedAt || headerObservedAt || reportedAt),
|
|
10163
10254
|
clientId: String(payload.clientId || payload.client_id || sources.clientId || headerClientId || userCtx?.userId || "anonymous").slice(0, 160),
|
|
10255
|
+
reportSource: String(payload.reportSource || payload.report_source || payload.source || "legacy").trim().toLowerCase().slice(0, 120) || "legacy",
|
|
10164
10256
|
userId: String(userCtx?.userId || payload.userId || payload.user_id || "").slice(0, 160),
|
|
10165
10257
|
baseRevision: String(payload.baseRevision || payload.base_revision || payload.expectedRevision || payload.expected_revision || rawSnapshot.baseRevision || sources.baseRevision || "").trim(),
|
|
10166
10258
|
scope: String(payload.scope || rawSnapshot.scope || rawSnapshot.next?.scope || sources.scope || "client").trim().toLowerCase() || "client",
|
|
@@ -10171,6 +10263,89 @@ function prdWorkflowSnapshotMetaFromReport(payload = {}, rawSnapshot = {}, req =
|
|
|
10171
10263
|
};
|
|
10172
10264
|
}
|
|
10173
10265
|
|
|
10266
|
+
function prdWorkflowStoreClientObservation({
|
|
10267
|
+
scopedRoot,
|
|
10268
|
+
tapdId,
|
|
10269
|
+
rawState,
|
|
10270
|
+
payload = {},
|
|
10271
|
+
req = null,
|
|
10272
|
+
userCtx = {},
|
|
10273
|
+
flowSource = "user",
|
|
10274
|
+
flowId = "",
|
|
10275
|
+
}) {
|
|
10276
|
+
const normalizedSnapshot = {
|
|
10277
|
+
...prdWorkflowSnapshotFromParsed(scopedRoot, tapdId, rawState, userCtx, { flowSource, flowId }),
|
|
10278
|
+
clientReportedAt: new Date().toISOString(),
|
|
10279
|
+
sources: {
|
|
10280
|
+
...(rawState.sources && typeof rawState.sources === "object" ? rawState.sources : {}),
|
|
10281
|
+
executionMode: "workflow-report",
|
|
10282
|
+
},
|
|
10283
|
+
};
|
|
10284
|
+
const reportMeta = prdWorkflowSnapshotMetaFromReport(payload, rawState, req, userCtx);
|
|
10285
|
+
const reportSource = {
|
|
10286
|
+
...(normalizedSnapshot.sources && typeof normalizedSnapshot.sources === "object" ? normalizedSnapshot.sources : {}),
|
|
10287
|
+
executionMode: "workflow-report",
|
|
10288
|
+
truth: "observation",
|
|
10289
|
+
authority: "client",
|
|
10290
|
+
persistence: "runtime",
|
|
10291
|
+
clientId: reportMeta.clientId,
|
|
10292
|
+
clientUserId: reportMeta.userId,
|
|
10293
|
+
clientReportedAt: reportMeta.reportedAt,
|
|
10294
|
+
clientObservedAt: reportMeta.observedAt,
|
|
10295
|
+
baseRevision: reportMeta.baseRevision,
|
|
10296
|
+
scope: reportMeta.scope,
|
|
10297
|
+
platform: reportMeta.platform,
|
|
10298
|
+
issueKey: reportMeta.issueKey,
|
|
10299
|
+
stageKey: reportMeta.stageKey,
|
|
10300
|
+
};
|
|
10301
|
+
const existingClientState = prdWorkflowReadClientState(scopedRoot, tapdId);
|
|
10302
|
+
const existingClientId = prdWorkflowSafeStateId(`${reportMeta.reportSource || "legacy"}:${reportMeta.clientId || "anonymous"}`);
|
|
10303
|
+
const previousClientSnapshot = existingClientState.clients?.[existingClientId]?.snapshot || null;
|
|
10304
|
+
const stampedSnapshot = prdWorkflowStampCurrentActionEntryTimes(
|
|
10305
|
+
scopedRoot,
|
|
10306
|
+
tapdId,
|
|
10307
|
+
normalizedSnapshot,
|
|
10308
|
+
existingClientState,
|
|
10309
|
+
reportMeta,
|
|
10310
|
+
);
|
|
10311
|
+
const storedObservationSnapshot = prdWorkflowStoredObservationSnapshot(stampedSnapshot, reportSource);
|
|
10312
|
+
const actionChanges = prdWorkflowSnapshotActionChanges(previousClientSnapshot || {}, storedObservationSnapshot);
|
|
10313
|
+
prdWorkflowWriteClientObservation(scopedRoot, tapdId, reportMeta, storedObservationSnapshot);
|
|
10314
|
+
prdWorkflowAppendAudit(scopedRoot, tapdId, {
|
|
10315
|
+
type: "workflow-report-observation-stored",
|
|
10316
|
+
flowSource,
|
|
10317
|
+
flowId,
|
|
10318
|
+
clientId: reportMeta.clientId,
|
|
10319
|
+
userId: reportMeta.userId,
|
|
10320
|
+
observedAt: reportMeta.observedAt,
|
|
10321
|
+
reportedAt: reportMeta.reportedAt,
|
|
10322
|
+
phase: String(storedObservationSnapshot?.phase || ""),
|
|
10323
|
+
pointer: String(storedObservationSnapshot?.pointer || ""),
|
|
10324
|
+
revision: String(storedObservationSnapshot?.revision || ""),
|
|
10325
|
+
actionCount: prdWorkflowSnapshotActionCount(storedObservationSnapshot),
|
|
10326
|
+
truth: "observation",
|
|
10327
|
+
authority: "client",
|
|
10328
|
+
persistence: "runtime",
|
|
10329
|
+
note: "producer observation accepted through the canonical Workflow Report endpoint",
|
|
10330
|
+
});
|
|
10331
|
+
for (const change of actionChanges) {
|
|
10332
|
+
prdWorkflowAppendAudit(scopedRoot, tapdId, {
|
|
10333
|
+
type: "snapshot-action-change",
|
|
10334
|
+
source: "workflow-report",
|
|
10335
|
+
clientId: reportMeta.clientId,
|
|
10336
|
+
userId: reportMeta.userId,
|
|
10337
|
+
observedAt: reportMeta.observedAt,
|
|
10338
|
+
reportedAt: reportMeta.reportedAt,
|
|
10339
|
+
revision: String(storedObservationSnapshot.revision || ""),
|
|
10340
|
+
previousRevision: String(previousClientSnapshot?.revision || ""),
|
|
10341
|
+
pointer: String(storedObservationSnapshot.pointer || ""),
|
|
10342
|
+
previousPointer: String(previousClientSnapshot?.pointer || ""),
|
|
10343
|
+
...change,
|
|
10344
|
+
});
|
|
10345
|
+
}
|
|
10346
|
+
return { reportMeta, storedObservationSnapshot, previousClientSnapshot };
|
|
10347
|
+
}
|
|
10348
|
+
|
|
10174
10349
|
function prdWorkflowSnapshotReportConflict(existingRecord, incomingSnapshot, meta) {
|
|
10175
10350
|
if (!existingRecord?.snapshot || meta.force) return null;
|
|
10176
10351
|
const current = existingRecord.snapshot;
|
|
@@ -10285,6 +10460,7 @@ function prdWorkflowMaterializeSnapshot(root, scopedRoot, tapdId, userCtx = {},
|
|
|
10285
10460
|
);
|
|
10286
10461
|
const clientObservations = prdWorkflowClientObservationRows(root, scopedRoot, tapdId).map((item) => ({
|
|
10287
10462
|
clientId: item.clientId,
|
|
10463
|
+
source: item.source || "legacy",
|
|
10288
10464
|
userId: item.userId || "",
|
|
10289
10465
|
phase: item.phase || "",
|
|
10290
10466
|
pointer: item.pointer || "",
|
|
@@ -10477,13 +10653,22 @@ function prdWorkflowNormalizeStoredRuntimeEvent(tapdId, event = {}) {
|
|
|
10477
10653
|
function prdWorkflowReadRuntimeEvents(scopedRoot, tapdId) {
|
|
10478
10654
|
try {
|
|
10479
10655
|
const p = prdWorkflowEventsPath(scopedRoot, tapdId);
|
|
10480
|
-
|
|
10481
|
-
const data = JSON.parse(fs.readFileSync(p, "utf-8"));
|
|
10482
|
-
const
|
|
10483
|
-
?
|
|
10484
|
-
.
|
|
10485
|
-
|
|
10656
|
+
const archivePath = prdWorkflowEventsArchivePath(scopedRoot, tapdId);
|
|
10657
|
+
const data = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf-8")) : {};
|
|
10658
|
+
const archivedEvents = fs.existsSync(archivePath)
|
|
10659
|
+
? fs.readFileSync(archivePath, "utf-8").split("\n").filter(Boolean).flatMap((line) => {
|
|
10660
|
+
try { return [JSON.parse(line)]; } catch { return []; }
|
|
10661
|
+
})
|
|
10486
10662
|
: [];
|
|
10663
|
+
const normalizedEvents = [...archivedEvents, ...(Array.isArray(data?.events) ? data.events : [])]
|
|
10664
|
+
.map((item) => prdWorkflowNormalizeStoredRuntimeEvent(data?.tapdId || tapdId, item))
|
|
10665
|
+
.filter((item) => item && typeof item === "object");
|
|
10666
|
+
const eventsByKey = new Map();
|
|
10667
|
+
for (const event of normalizedEvents) {
|
|
10668
|
+
const key = `${prdWorkflowRuntimeEventProducer(event)}:${prdWorkflowRuntimeEventOperation(event)}:${String(event.id || "")}`;
|
|
10669
|
+
eventsByKey.set(key, event);
|
|
10670
|
+
}
|
|
10671
|
+
const events = [...eventsByKey.values()];
|
|
10487
10672
|
return {
|
|
10488
10673
|
version: 1,
|
|
10489
10674
|
tapdId: String(data?.tapdId || tapdId || ""),
|
|
@@ -10498,11 +10683,17 @@ function prdWorkflowReadRuntimeEvents(scopedRoot, tapdId) {
|
|
|
10498
10683
|
function prdWorkflowWriteRuntimeEvents(scopedRoot, tapdId, events) {
|
|
10499
10684
|
const p = prdWorkflowEventsPath(scopedRoot, tapdId);
|
|
10500
10685
|
fs.mkdirSync(path.dirname(p), { recursive: true });
|
|
10686
|
+
const allEvents = Array.isArray(events) ? events : [];
|
|
10687
|
+
const overflow = allEvents.slice(0, Math.max(0, allEvents.length - PRD_WORKFLOW_RUNTIME_EVENTS_MAX));
|
|
10688
|
+
const archivePath = prdWorkflowEventsArchivePath(scopedRoot, tapdId);
|
|
10689
|
+
const archiveTmp = `${archivePath}.${process.pid}.${Date.now()}.tmp`;
|
|
10690
|
+
fs.writeFileSync(archiveTmp, overflow.length ? `${overflow.map((event) => JSON.stringify(event)).join("\n")}\n` : "", "utf-8");
|
|
10691
|
+
fs.renameSync(archiveTmp, archivePath);
|
|
10501
10692
|
const data = {
|
|
10502
10693
|
version: 1,
|
|
10503
10694
|
tapdId: String(tapdId || ""),
|
|
10504
10695
|
updatedAt: new Date().toISOString(),
|
|
10505
|
-
events:
|
|
10696
|
+
events: allEvents.slice(-PRD_WORKFLOW_RUNTIME_EVENTS_MAX),
|
|
10506
10697
|
};
|
|
10507
10698
|
const tmp = `${p}.${process.pid}.${Date.now()}.tmp`;
|
|
10508
10699
|
fs.writeFileSync(tmp, JSON.stringify(data, null, 2) + "\n", "utf-8");
|
|
@@ -10565,6 +10756,19 @@ function prdWorkflowRuntimeEventCanonicalAction(stage = "") {
|
|
|
10565
10756
|
: "";
|
|
10566
10757
|
}
|
|
10567
10758
|
|
|
10759
|
+
function prdWorkflowRuntimeEventProducer(event = {}) {
|
|
10760
|
+
return String(event.source || event.producer || "agentflow")
|
|
10761
|
+
.trim()
|
|
10762
|
+
.toLowerCase()
|
|
10763
|
+
.slice(0, 120) || "agentflow";
|
|
10764
|
+
}
|
|
10765
|
+
|
|
10766
|
+
function prdWorkflowRuntimeEventOperation(event = {}) {
|
|
10767
|
+
return String(event.operation || (event.type === "review-link" ? "artifact.publish" : "report"))
|
|
10768
|
+
.trim()
|
|
10769
|
+
.toLowerCase() || "report";
|
|
10770
|
+
}
|
|
10771
|
+
|
|
10568
10772
|
function prdWorkflowRuntimeOwnedArtifacts(values, stage = "") {
|
|
10569
10773
|
if (!Array.isArray(values)) return values;
|
|
10570
10774
|
const ownsOnlyChangedArtifact = /^(?:issue-plan|implementation|bugfix|integration):/.test(String(stage || ""));
|
|
@@ -10586,16 +10790,21 @@ function prdWorkflowRuntimeEventId(event = {}) {
|
|
|
10586
10790
|
const scope = String(event.scope || "").trim();
|
|
10587
10791
|
const platform = String(event.platform || "").trim();
|
|
10588
10792
|
const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
|
|
10793
|
+
const operation = prdWorkflowRuntimeEventOperation(event);
|
|
10794
|
+
const stableActionKey = String(event?.actionModel?.key || "").trim();
|
|
10795
|
+
if (stableActionKey && aggregateByStage && String(event?.type || "") === "workflow-report") {
|
|
10796
|
+
return `stage_${prdWorkflowSafeStateId([prdWorkflowRuntimeEventProducer(event), operation, stableActionKey].join(":"))}`;
|
|
10797
|
+
}
|
|
10589
10798
|
if ((stage || action) && aggregateByStage) {
|
|
10590
10799
|
const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
|
|
10591
|
-
const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
10800
|
+
const key = [prdWorkflowRuntimeEventProducer(event), operation, scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
10592
10801
|
return `stage_${prdWorkflowSafeStateId(key)}`;
|
|
10593
10802
|
}
|
|
10594
10803
|
const existing = String(event.id || event.eventId || event.event_id || "").trim();
|
|
10595
10804
|
if (existing) return existing.slice(0, 160);
|
|
10596
10805
|
if (stage || action) {
|
|
10597
10806
|
const issue = String(event.issueKey || event.issue_key || event.issue || "").trim();
|
|
10598
|
-
const key = [scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
10807
|
+
const key = [prdWorkflowRuntimeEventProducer(event), operation, scope, issue, platform, stage || action].filter(Boolean).join(":");
|
|
10599
10808
|
return `stage_${prdWorkflowSafeStateId(key)}`;
|
|
10600
10809
|
}
|
|
10601
10810
|
return `evt_${Date.now().toString(36)}_${crypto.randomBytes(4).toString("hex")}`;
|
|
@@ -10659,8 +10868,13 @@ function prdWorkflowNormalizeRuntimeEvent(tapdId, event = {}) {
|
|
|
10659
10868
|
entry.stage = stage;
|
|
10660
10869
|
entry.stageKey = stage;
|
|
10661
10870
|
}
|
|
10662
|
-
|
|
10663
|
-
|
|
10871
|
+
const attachProducer = (values) => (Array.isArray(values) ? values.map((item) => (
|
|
10872
|
+
item && typeof item === "object" && !Array.isArray(item)
|
|
10873
|
+
? { ...item, producer: String(item.producer || source).trim().toLowerCase() || source }
|
|
10874
|
+
: item
|
|
10875
|
+
)) : values);
|
|
10876
|
+
entry.artifacts = attachProducer(prdWorkflowRuntimeOwnedArtifacts(entry.artifacts, stage));
|
|
10877
|
+
entry.links = attachProducer(prdWorkflowRuntimeOwnedArtifacts(entry.links, stage));
|
|
10664
10878
|
if (scope) entry.scope = scope;
|
|
10665
10879
|
if (platform) entry.platform = platform;
|
|
10666
10880
|
if (!entry.createdAt) entry.createdAt = event.startedAt || now;
|
|
@@ -10759,8 +10973,14 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
|
10759
10973
|
const current = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId);
|
|
10760
10974
|
const entry = prdWorkflowNormalizeRuntimeEvent(tapdId, event);
|
|
10761
10975
|
const entryIdem = String(entry.idempotencyKey || "").trim();
|
|
10976
|
+
const entryProducer = prdWorkflowRuntimeEventProducer(entry);
|
|
10977
|
+
const entryOperation = prdWorkflowRuntimeEventOperation(entry);
|
|
10978
|
+
const entryDedupeKey = prdWorkflowRuntimeEventDedupeKey(entry);
|
|
10762
10979
|
const index = current.events.findIndex((item) => {
|
|
10980
|
+
if (prdWorkflowRuntimeEventProducer(item) !== entryProducer) return false;
|
|
10763
10981
|
if (String(item?.id || "") === entry.id) return true;
|
|
10982
|
+
if (prdWorkflowRuntimeEventOperation(item) !== entryOperation) return false;
|
|
10983
|
+
if (prdWorkflowRuntimeEventDedupeKey(item) === entryDedupeKey) return true;
|
|
10764
10984
|
if (!entryIdem) return false;
|
|
10765
10985
|
if (String(item?.idempotencyKey || "").trim() === entryIdem) return true;
|
|
10766
10986
|
return Array.isArray(item?.idempotencyHistory) && item.idempotencyHistory.includes(entryIdem);
|
|
@@ -10795,17 +11015,43 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
|
10795
11015
|
incomingGlobalStatePatch,
|
|
10796
11016
|
)
|
|
10797
11017
|
: previousGlobalStatePatch;
|
|
11018
|
+
const incomingPatchPaths = Array.isArray(entry?.globalStateOwnerPaths) ? entry.globalStateOwnerPaths : [];
|
|
11019
|
+
const previousRemovePaths = Array.isArray(events[index]?.globalStateRemove || events[index]?.global_state_remove)
|
|
11020
|
+
? (events[index].globalStateRemove || events[index].global_state_remove)
|
|
11021
|
+
: [];
|
|
11022
|
+
const incomingRemovePaths = Array.isArray(entry?.globalStateRemove || entry?.global_state_remove)
|
|
11023
|
+
? (entry.globalStateRemove || entry.global_state_remove)
|
|
11024
|
+
: [];
|
|
11025
|
+
const overlapsPath = (left, right) => left === right || left.startsWith(`${right}.`) || right.startsWith(`${left}.`);
|
|
11026
|
+
const mergedGlobalStateRemove = [...new Set([
|
|
11027
|
+
...previousRemovePaths.filter((removedPath) => !incomingPatchPaths.some((patchPath) => overlapsPath(String(removedPath), String(patchPath)))),
|
|
11028
|
+
...incomingRemovePaths,
|
|
11029
|
+
])];
|
|
11030
|
+
let normalizedGlobalStatePatch = mergedGlobalStatePatch;
|
|
11031
|
+
for (const removedPath of incomingRemovePaths) {
|
|
11032
|
+
normalizedGlobalStatePatch = removeWorkflowGlobalStatePath(normalizedGlobalStatePatch, removedPath);
|
|
11033
|
+
}
|
|
10798
11034
|
const prevArtifact = prdWorkflowRuntimeEventArtifactSignature(events[index]);
|
|
10799
11035
|
const nextArtifact = prdWorkflowRuntimeEventArtifactSignature(entry);
|
|
10800
11036
|
artifactConflict = Boolean(prevArtifact && nextArtifact && prevArtifact !== nextArtifact &&
|
|
10801
11037
|
prdWorkflowRuntimeEventShouldConflictOnArtifact(events[index]) &&
|
|
10802
11038
|
prdWorkflowRuntimeEventShouldConflictOnArtifact(entry));
|
|
10803
|
-
const idempotencyHistory = [
|
|
10804
|
-
...(events[index].idempotencyKey ? [events[index].idempotencyKey] : []),
|
|
10805
|
-
...(entry.idempotencyKey ? [entry.idempotencyKey] : []),
|
|
11039
|
+
const idempotencyHistory = [...new Set([
|
|
10806
11040
|
...(Array.isArray(events[index].idempotencyHistory) ? events[index].idempotencyHistory : []),
|
|
11041
|
+
...(events[index].idempotencyKey ? [events[index].idempotencyKey] : []),
|
|
10807
11042
|
...(Array.isArray(entry.idempotencyHistory) ? entry.idempotencyHistory : []),
|
|
10808
|
-
|
|
11043
|
+
...(entry.idempotencyKey ? [entry.idempotencyKey] : []),
|
|
11044
|
+
])].slice(-50);
|
|
11045
|
+
const allIdempotencyFingerprints = {
|
|
11046
|
+
...(events[index].idempotencyFingerprints || {}),
|
|
11047
|
+
...(events[index].idempotencyKey && events[index].idempotencyFingerprint
|
|
11048
|
+
? { [events[index].idempotencyKey]: events[index].idempotencyFingerprint }
|
|
11049
|
+
: {}),
|
|
11050
|
+
...(entry.idempotencyFingerprints || {}),
|
|
11051
|
+
...(entry.idempotencyKey && entry.idempotencyFingerprint
|
|
11052
|
+
? { [entry.idempotencyKey]: entry.idempotencyFingerprint }
|
|
11053
|
+
: {}),
|
|
11054
|
+
};
|
|
10809
11055
|
events[index] = {
|
|
10810
11056
|
...events[index],
|
|
10811
11057
|
...entry,
|
|
@@ -10814,19 +11060,28 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
|
10814
11060
|
outputs: prdWorkflowMergeRuntimeEventArrays(events[index].outputs, entry.outputs),
|
|
10815
11061
|
results: prdWorkflowMergeRuntimeEventArrays(events[index].results, entry.results),
|
|
10816
11062
|
actionModel: mergeWorkflowGlobalState(events[index].actionModel, entry.actionModel),
|
|
10817
|
-
globalStateRemove:
|
|
10818
|
-
|
|
10819
|
-
|
|
11063
|
+
globalStateRemove: mergedGlobalStateRemove,
|
|
11064
|
+
extensionsPatch: entry.extensionsPatch
|
|
11065
|
+
? mergeWorkflowGlobalState(events[index].extensionsPatch, entry.extensionsPatch)
|
|
11066
|
+
: events[index].extensionsPatch,
|
|
11067
|
+
globalStateOwnerPaths: prdWorkflowMergeRuntimeEventArrays(
|
|
11068
|
+
events[index].globalStateOwnerPaths,
|
|
11069
|
+
entry.globalStateOwnerPaths,
|
|
10820
11070
|
),
|
|
10821
11071
|
createdAt: events[index].createdAt || entry.createdAt,
|
|
10822
11072
|
startedAt: events[index].startedAt || entry.startedAt,
|
|
10823
|
-
idempotencyHistory
|
|
11073
|
+
idempotencyHistory,
|
|
11074
|
+
idempotencyFingerprints: Object.fromEntries(
|
|
11075
|
+
idempotencyHistory
|
|
11076
|
+
.filter((key) => allIdempotencyFingerprints[key])
|
|
11077
|
+
.map((key) => [key, allIdempotencyFingerprints[key]]),
|
|
11078
|
+
),
|
|
10824
11079
|
};
|
|
10825
11080
|
if (mergedImplementationMetadata && typeof mergedImplementationMetadata === "object" && !Array.isArray(mergedImplementationMetadata)) {
|
|
10826
11081
|
events[index].implementationMetadata = mergedImplementationMetadata;
|
|
10827
11082
|
}
|
|
10828
|
-
if (
|
|
10829
|
-
events[index].globalStatePatch =
|
|
11083
|
+
if (normalizedGlobalStatePatch && typeof normalizedGlobalStatePatch === "object" && !Array.isArray(normalizedGlobalStatePatch)) {
|
|
11084
|
+
events[index].globalStatePatch = normalizedGlobalStatePatch;
|
|
10830
11085
|
}
|
|
10831
11086
|
if (artifactConflict) {
|
|
10832
11087
|
events[index] = {
|
|
@@ -10871,36 +11126,128 @@ function prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, event = {}) {
|
|
|
10871
11126
|
}
|
|
10872
11127
|
}
|
|
10873
11128
|
|
|
10874
|
-
function
|
|
11129
|
+
function prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "", completedOnly = true, operation = "") {
|
|
10875
11130
|
const key = String(idempotencyKey || "").trim();
|
|
10876
11131
|
if (!key) return null;
|
|
11132
|
+
const producer = String(source || "").trim().toLowerCase();
|
|
11133
|
+
const operationKey = String(operation || "").trim().toLowerCase();
|
|
10877
11134
|
const events = prdWorkflowReadRuntimeEvents(scopedRoot, tapdId).events;
|
|
10878
11135
|
return [...events].reverse().find((event) => (
|
|
11136
|
+
(!producer || prdWorkflowRuntimeEventProducer(event) === producer) &&
|
|
11137
|
+
(!operationKey || String(event?.operation || (event?.type === "review-link" ? "artifact.publish" : "report")).toLowerCase() === operationKey) &&
|
|
10879
11138
|
(String(event?.idempotencyKey || "") === key || (Array.isArray(event?.idempotencyHistory) && event.idempotencyHistory.includes(key))) &&
|
|
10880
|
-
["done", "success", "completed"].includes(String(event?.status || "").toLowerCase())
|
|
11139
|
+
(!completedOnly || ["done", "success", "completed"].includes(String(event?.status || "").toLowerCase()))
|
|
10881
11140
|
)) || null;
|
|
10882
11141
|
}
|
|
10883
11142
|
|
|
11143
|
+
function prdWorkflowFindCompletedIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source = "") {
|
|
11144
|
+
return prdWorkflowFindIdempotencyEvent(scopedRoot, tapdId, idempotencyKey, source, false, "report");
|
|
11145
|
+
}
|
|
11146
|
+
|
|
11147
|
+
function prdWorkflowIdempotencyFingerprint(event = {}, idempotencyKey = "") {
|
|
11148
|
+
const key = String(idempotencyKey || "").trim();
|
|
11149
|
+
return String(
|
|
11150
|
+
event?.idempotencyFingerprints?.[key]
|
|
11151
|
+
|| (String(event?.idempotencyKey || "").trim() === key ? event?.idempotencyFingerprint : "")
|
|
11152
|
+
|| "",
|
|
11153
|
+
);
|
|
11154
|
+
}
|
|
11155
|
+
|
|
11156
|
+
function prdWorkflowResourceVersionConflicts(expectedVersions = {}, currentVersions = {}) {
|
|
11157
|
+
const conflicts = [];
|
|
11158
|
+
for (const [resourceKey, expectedVersion] of Object.entries(expectedVersions || {})) {
|
|
11159
|
+
const currentVersion = String(currentVersions?.[resourceKey] || "absent");
|
|
11160
|
+
const expected = String(expectedVersion || "absent");
|
|
11161
|
+
if (expected === currentVersion) continue;
|
|
11162
|
+
conflicts.push({ resourceKey, expectedVersion: expected, currentVersion });
|
|
11163
|
+
}
|
|
11164
|
+
return conflicts;
|
|
11165
|
+
}
|
|
11166
|
+
|
|
11167
|
+
function prdWorkflowGlobalPathOwners(snapshot = {}) {
|
|
11168
|
+
const owners = new Map();
|
|
11169
|
+
for (const event of Array.isArray(snapshot.runtimeEvents) ? snapshot.runtimeEvents : []) {
|
|
11170
|
+
const source = prdWorkflowRuntimeEventProducer(event);
|
|
11171
|
+
for (const path of Array.isArray(event?.globalStateOwnerPaths) ? event.globalStateOwnerPaths : []) {
|
|
11172
|
+
const normalized = String(path || "").trim();
|
|
11173
|
+
if (normalized) owners.set(normalized, source);
|
|
11174
|
+
}
|
|
11175
|
+
}
|
|
11176
|
+
return owners;
|
|
11177
|
+
}
|
|
11178
|
+
|
|
11179
|
+
function prdWorkflowGlobalOwnershipConflicts(report, currentSnapshot = {}) {
|
|
11180
|
+
const source = prdWorkflowRuntimeEventProducer(report?.event || {});
|
|
11181
|
+
const owners = prdWorkflowGlobalPathOwners(currentSnapshot);
|
|
11182
|
+
const conflicts = [];
|
|
11183
|
+
for (const path of Array.isArray(report?.event?.globalStateOwnerPaths) ? report.event.globalStateOwnerPaths : []) {
|
|
11184
|
+
const normalizedPath = String(path || "");
|
|
11185
|
+
const match = [...owners.entries()].find(([ownedPath, owner]) => (
|
|
11186
|
+
owner !== source && (
|
|
11187
|
+
ownedPath === normalizedPath ||
|
|
11188
|
+
ownedPath.startsWith(`${normalizedPath}.`) ||
|
|
11189
|
+
normalizedPath.startsWith(`${ownedPath}.`)
|
|
11190
|
+
)
|
|
11191
|
+
));
|
|
11192
|
+
if (match) conflicts.push({ path: normalizedPath, owner: match[1], source });
|
|
11193
|
+
}
|
|
11194
|
+
return conflicts;
|
|
11195
|
+
}
|
|
11196
|
+
|
|
11197
|
+
function prdWorkflowMergeProducerTimeline(report, currentSnapshot = {}) {
|
|
11198
|
+
if (!report?.projections || !Array.isArray(report.projections.timeline)) return report;
|
|
11199
|
+
const source = prdWorkflowRuntimeEventProducer(report.event);
|
|
11200
|
+
const current = Array.isArray(currentSnapshot?.projections?.timeline) ? currentSnapshot.projections.timeline : [];
|
|
11201
|
+
const incoming = report.projections.timeline;
|
|
11202
|
+
const foreignCurrent = current.filter((item) => prdWorkflowRuntimeEventProducer(item) !== source);
|
|
11203
|
+
const ownIncoming = incoming.filter((item) => prdWorkflowRuntimeEventProducer(item) === source);
|
|
11204
|
+
const foreignIncoming = incoming.filter((item) => prdWorkflowRuntimeEventProducer(item) !== source);
|
|
11205
|
+
const foreignByKey = new Map(foreignCurrent.map((item) => [String(item?.key || `${item?.source}:${item?.kind}:${item?.id}`), item]));
|
|
11206
|
+
for (const item of foreignIncoming) {
|
|
11207
|
+
const key = String(item?.key || `${item?.source}:${item?.kind}:${item?.id}`);
|
|
11208
|
+
const existing = foreignByKey.get(key);
|
|
11209
|
+
const existingVersion = existing
|
|
11210
|
+
? Object.values(workflowSnapshotResourceVersions({ projections: { timeline: [existing] } }))[0]
|
|
11211
|
+
: "";
|
|
11212
|
+
const incomingVersion = Object.values(workflowSnapshotResourceVersions({ projections: { timeline: [item] } }))[0] || "";
|
|
11213
|
+
if (!existing || existingVersion !== incomingVersion) {
|
|
11214
|
+
return { error: `projections.timeline may not modify entries owned by source ${item?.source || "unknown"}` };
|
|
11215
|
+
}
|
|
11216
|
+
}
|
|
11217
|
+
const timeline = [...foreignCurrent, ...ownIncoming];
|
|
11218
|
+
return {
|
|
11219
|
+
...report,
|
|
11220
|
+
projections: { ...report.projections, timeline },
|
|
11221
|
+
event: { ...report.event, projections: { ...report.event.projections, timeline } },
|
|
11222
|
+
};
|
|
11223
|
+
}
|
|
11224
|
+
|
|
10884
11225
|
function prdWorkflowRuntimeEventDedupeKey(event = {}, index = 0) {
|
|
11226
|
+
const producer = prdWorkflowRuntimeEventProducer(event);
|
|
11227
|
+
const operation = prdWorkflowRuntimeEventOperation(event);
|
|
10885
11228
|
const stage = prdWorkflowRuntimeEventCanonicalStage(event);
|
|
10886
11229
|
const aggregateByStage = event.aggregateByStage !== false && event.aggregate_by_stage !== false;
|
|
11230
|
+
const stableActionKey = String(event?.actionModel?.key || "").trim();
|
|
11231
|
+
if (stableActionKey && aggregateByStage && String(event?.type || "") === "workflow-report") {
|
|
11232
|
+
return `producer:${producer}:operation:${operation}:action:${stableActionKey}`;
|
|
11233
|
+
}
|
|
10887
11234
|
if (stage) {
|
|
10888
11235
|
const issue = event?.issueKey || event?.issue_key || event?.issue;
|
|
10889
11236
|
const platform = event?.platform;
|
|
10890
11237
|
if (aggregateByStage || issue || platform) {
|
|
10891
|
-
return ["stage", event?.scope, issue, platform, stage]
|
|
11238
|
+
return ["producer", producer, "operation", operation, "stage", event?.scope, issue, platform, stage]
|
|
10892
11239
|
.map((value) => String(value || "").trim())
|
|
10893
11240
|
.join(":");
|
|
10894
11241
|
}
|
|
10895
11242
|
}
|
|
10896
11243
|
const id = String(event?.id || event?.eventId || event?.event_id || "").trim();
|
|
10897
|
-
if (id) return `id:${id}`;
|
|
11244
|
+
if (id) return `producer:${producer}:operation:${operation}:id:${id}`;
|
|
10898
11245
|
if (stage) {
|
|
10899
|
-
return ["stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
|
|
11246
|
+
return ["producer", producer, "operation", operation, "stage", event?.scope, event?.issueKey || event?.issue_key || event?.issue, event?.platform, stage]
|
|
10900
11247
|
.map((value) => String(value || "").trim())
|
|
10901
11248
|
.join(":");
|
|
10902
11249
|
}
|
|
10903
|
-
return `idx:${index}`;
|
|
11250
|
+
return `producer:${producer}:operation:${operation}:idx:${index}`;
|
|
10904
11251
|
}
|
|
10905
11252
|
|
|
10906
11253
|
function prdWorkflowMergeRuntimeEventList(snapshotEvents = [], runtimeEvents = []) {
|
|
@@ -11169,14 +11516,24 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
|
11169
11516
|
const globalState = prdWorkflowGlobalStateFromEvents(tapdId, snapshot, runtimeEvents);
|
|
11170
11517
|
const artifacts = mergeWorkflowArtifacts(snapshot?.artifacts, runtimeEvents);
|
|
11171
11518
|
const projections = materializeWorkflowProjections(snapshot, runtimeEvents);
|
|
11172
|
-
|
|
11519
|
+
const extensions = materializeWorkflowExtensions(snapshot, runtimeEvents);
|
|
11520
|
+
const prdFlowExtension = extensions["prd-flow"] && typeof extensions["prd-flow"] === "object"
|
|
11521
|
+
? extensions["prd-flow"]
|
|
11522
|
+
: {};
|
|
11523
|
+
const prdFlowExtensionView = {};
|
|
11524
|
+
for (const key of ["issues", "issueGroups", "issue_groups", "epics", "epicGroups", "epic_groups", "aiDocs", "ai_docs"]) {
|
|
11525
|
+
if (Object.prototype.hasOwnProperty.call(prdFlowExtension, key)) prdFlowExtensionView[key] = prdFlowExtension[key];
|
|
11526
|
+
}
|
|
11527
|
+
const materialized = {
|
|
11173
11528
|
...snapshot,
|
|
11529
|
+
...prdFlowExtensionView,
|
|
11174
11530
|
workflow: globalState.workflow,
|
|
11175
11531
|
overall,
|
|
11176
11532
|
globalState,
|
|
11177
11533
|
artifacts,
|
|
11178
11534
|
projections,
|
|
11179
|
-
|
|
11535
|
+
extensions,
|
|
11536
|
+
runtimeRevision: workflowRuntimeRevision(globalState, artifacts, runtimeEvents, projections, extensions),
|
|
11180
11537
|
runtimeEvents,
|
|
11181
11538
|
events,
|
|
11182
11539
|
sources: {
|
|
@@ -11184,6 +11541,8 @@ function prdWorkflowMergeRuntimeEvents(scopedRoot, tapdId, snapshot) {
|
|
|
11184
11541
|
runtimeEventsUpdatedAt: runtime.updatedAt || "",
|
|
11185
11542
|
},
|
|
11186
11543
|
};
|
|
11544
|
+
materialized.resourceVersions = workflowSnapshotResourceVersions(materialized);
|
|
11545
|
+
return materialized;
|
|
11187
11546
|
}
|
|
11188
11547
|
|
|
11189
11548
|
function prdWorkflowMockSnapshot(scopedRoot, tapdId = "mock-prd") {
|
|
@@ -12587,6 +12946,95 @@ export function startUiServer({
|
|
|
12587
12946
|
json(res, 200, { token: getSessionTokenFromRequest(req) || "" });
|
|
12588
12947
|
return;
|
|
12589
12948
|
}
|
|
12949
|
+
if (req.method === "POST" && url.pathname === "/api/workflows/access/sync") {
|
|
12950
|
+
if (!authUser?.userId) {
|
|
12951
|
+
json(res, 401, { error: "Authentication required" });
|
|
12952
|
+
return;
|
|
12953
|
+
}
|
|
12954
|
+
let payload;
|
|
12955
|
+
try {
|
|
12956
|
+
payload = JSON.parse(await readBody(req, 256 * 1024));
|
|
12957
|
+
} catch (error) {
|
|
12958
|
+
json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
|
|
12959
|
+
return;
|
|
12960
|
+
}
|
|
12961
|
+
try {
|
|
12962
|
+
const workflow = normalizeWorkflowReference(payload);
|
|
12963
|
+
if (workflow.error) {
|
|
12964
|
+
json(res, 400, { error: workflow.error });
|
|
12965
|
+
return;
|
|
12966
|
+
}
|
|
12967
|
+
if (workflow.namespace !== "tapd") {
|
|
12968
|
+
json(res, 400, { error: `Unsupported Workflow authority namespace: ${workflow.namespace}` });
|
|
12969
|
+
return;
|
|
12970
|
+
}
|
|
12971
|
+
const authorityPayload = payload?.authority && typeof payload.authority === "object" && !Array.isArray(payload.authority)
|
|
12972
|
+
? payload.authority
|
|
12973
|
+
: {};
|
|
12974
|
+
const authorityType = String(authorityPayload.type || payload.authorityType || "tapd").trim().toLowerCase();
|
|
12975
|
+
const ownerIdentity = workflowAuthorityIdentity(authorityPayload.owner ?? payload.owner);
|
|
12976
|
+
const participantIdentities = workflowAuthorityIdentities(authorityPayload.participants ?? payload.participants);
|
|
12977
|
+
if (!ownerIdentity) {
|
|
12978
|
+
json(res, 400, { error: "authority.owner is required" });
|
|
12979
|
+
return;
|
|
12980
|
+
}
|
|
12981
|
+
const ownerUser = findWorkspaceShareUser(ownerIdentity);
|
|
12982
|
+
if (!ownerUser) {
|
|
12983
|
+
json(res, 422, {
|
|
12984
|
+
error: "TAPD owner has not registered or logged in to AgentFlow",
|
|
12985
|
+
owner: ownerIdentity,
|
|
12986
|
+
});
|
|
12987
|
+
return;
|
|
12988
|
+
}
|
|
12989
|
+
const resolvedParticipants = [];
|
|
12990
|
+
const unresolvedParticipants = [];
|
|
12991
|
+
for (const identity of participantIdentities) {
|
|
12992
|
+
const user = findWorkspaceShareUser(identity);
|
|
12993
|
+
if (user) resolvedParticipants.push(user);
|
|
12994
|
+
else unresolvedParticipants.push(identity);
|
|
12995
|
+
}
|
|
12996
|
+
const result = syncPrdWorkflowAuthority({
|
|
12997
|
+
tapdId: workflow.id,
|
|
12998
|
+
userId: userCtx.userId,
|
|
12999
|
+
isAdmin: userCtx.isAdmin === true,
|
|
13000
|
+
authority: authorityType,
|
|
13001
|
+
ownerUserId: ownerUser.userId,
|
|
13002
|
+
ownerIdentity,
|
|
13003
|
+
participantUserIds: resolvedParticipants.map((user) => user.userId),
|
|
13004
|
+
participantIdentities,
|
|
13005
|
+
unresolvedParticipants,
|
|
13006
|
+
observedAt: authorityPayload.observedAt || authorityPayload.observed_at || payload.observedAt || payload.observed_at,
|
|
13007
|
+
revision: authorityPayload.revision || payload.revision,
|
|
13008
|
+
});
|
|
13009
|
+
if (result.error) {
|
|
13010
|
+
json(res, result.status || 400, { error: result.error });
|
|
13011
|
+
return;
|
|
13012
|
+
}
|
|
13013
|
+
const collaboration = prdWorkflowCollaborationSummaryWithUsers(result.record, userCtx.userId);
|
|
13014
|
+
prdWorkflowBroadcast(prdWorkflowKey(userCtx, "", "", workflow.id), {
|
|
13015
|
+
type: "authority.synced",
|
|
13016
|
+
tapdId: workflow.id,
|
|
13017
|
+
ownerId: result.record.ownerId,
|
|
13018
|
+
});
|
|
13019
|
+
json(res, 200, {
|
|
13020
|
+
ok: true,
|
|
13021
|
+
workflow,
|
|
13022
|
+
created: result.created === true,
|
|
13023
|
+
ownerChanged: result.ownerChanged === true,
|
|
13024
|
+
collaboration,
|
|
13025
|
+
matchedParticipants: resolvedParticipants.map((user) => ({
|
|
13026
|
+
userId: user.userId,
|
|
13027
|
+
username: user.username,
|
|
13028
|
+
role: "viewer",
|
|
13029
|
+
source: "tapd",
|
|
13030
|
+
})),
|
|
13031
|
+
unresolvedParticipants,
|
|
13032
|
+
});
|
|
13033
|
+
} catch (error) {
|
|
13034
|
+
json(res, 500, { error: (error && error.message) || String(error) });
|
|
13035
|
+
}
|
|
13036
|
+
return;
|
|
13037
|
+
}
|
|
12590
13038
|
if (req.method === "GET" && url.pathname === "/api/prd-workflows") {
|
|
12591
13039
|
if (!authUser?.userId) {
|
|
12592
13040
|
json(res, 401, { error: "Unauthorized" });
|
|
@@ -12610,7 +13058,7 @@ export function startUiServer({
|
|
|
12610
13058
|
records = listPrdWorkflowCollaborationsForUser(userCtx.userId);
|
|
12611
13059
|
}
|
|
12612
13060
|
const workflows = records.map((record) => {
|
|
12613
|
-
const stateRoot = path.resolve(getAgentflowUserDataRoot(record.ownerId));
|
|
13061
|
+
const stateRoot = path.resolve(getAgentflowUserDataRoot(record.stateOwnerId || record.ownerId));
|
|
12614
13062
|
const tapdId = String(record.tapdId || "").trim();
|
|
12615
13063
|
const project = prdWorkflowReadProjectState(stateRoot, tapdId);
|
|
12616
13064
|
const latestClient = prdWorkflowLatestClientSnapshot(stateRoot, stateRoot, tapdId);
|
|
@@ -12803,7 +13251,12 @@ export function startUiServer({
|
|
|
12803
13251
|
json(res, 200, {
|
|
12804
13252
|
ok: true,
|
|
12805
13253
|
collaboration: prdWorkflowCollaborationSummaryWithUsers(record, userCtx.userId),
|
|
12806
|
-
member: {
|
|
13254
|
+
member: {
|
|
13255
|
+
userId: targetUser.userId,
|
|
13256
|
+
username: targetUser.username,
|
|
13257
|
+
role: payload?.role === "viewer" ? "viewer" : "reporter",
|
|
13258
|
+
source: "explicit",
|
|
13259
|
+
},
|
|
12807
13260
|
});
|
|
12808
13261
|
} catch (error) {
|
|
12809
13262
|
json(res, 400, { error: (error && error.message) || String(error) });
|
|
@@ -12955,6 +13408,8 @@ export function startUiServer({
|
|
|
12955
13408
|
}
|
|
12956
13409
|
|
|
12957
13410
|
if (req.method === "POST" && url.pathname === "/api/prd-workflow/snapshot") {
|
|
13411
|
+
res.setHeader("Deprecation", "true");
|
|
13412
|
+
res.setHeader("Link", "</api/workflows/report>; rel=\"successor-version\"");
|
|
12958
13413
|
if (!authUser?.userId) {
|
|
12959
13414
|
json(res, 401, { error: "Authentication required" });
|
|
12960
13415
|
return;
|
|
@@ -13030,7 +13485,7 @@ export function startUiServer({
|
|
|
13030
13485
|
stageKey: reportMeta.stageKey,
|
|
13031
13486
|
};
|
|
13032
13487
|
const existingClientState = prdWorkflowReadClientState(scopedRoot, tapdId);
|
|
13033
|
-
const existingClientId = prdWorkflowSafeStateId(reportMeta.clientId || "anonymous");
|
|
13488
|
+
const existingClientId = prdWorkflowSafeStateId(`${reportMeta.reportSource || "legacy"}:${reportMeta.clientId || "anonymous"}`);
|
|
13034
13489
|
const previousClientSnapshot = existingClientState.clients?.[existingClientId]?.snapshot || null;
|
|
13035
13490
|
const stampedSnapshot = prdWorkflowStampCurrentActionEntryTimes(
|
|
13036
13491
|
scopedRoot,
|
|
@@ -13188,6 +13643,10 @@ export function startUiServer({
|
|
|
13188
13643
|
json(res, 200, {
|
|
13189
13644
|
ok: true,
|
|
13190
13645
|
snapshot: withDiagnostic,
|
|
13646
|
+
compatibility: {
|
|
13647
|
+
deprecatedEndpoint: "/api/prd-workflow/snapshot",
|
|
13648
|
+
replacement: "/api/workflows/report with observation.state",
|
|
13649
|
+
},
|
|
13191
13650
|
...(workflowShare ? { workflowShare, shareUrl: workflowShare.shortUrl || workflowShare.url } : {}),
|
|
13192
13651
|
});
|
|
13193
13652
|
} catch (e) {
|
|
@@ -13701,13 +14160,14 @@ export function startUiServer({
|
|
|
13701
14160
|
}
|
|
13702
14161
|
let payload;
|
|
13703
14162
|
try {
|
|
13704
|
-
payload = JSON.parse(await readBody(req));
|
|
13705
|
-
} catch {
|
|
13706
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
14163
|
+
payload = JSON.parse(await readBody(req, 1024 * 1024));
|
|
14164
|
+
} catch (error) {
|
|
14165
|
+
json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
|
|
13707
14166
|
return;
|
|
13708
14167
|
}
|
|
14168
|
+
let releaseWorkflowWriteLock = null;
|
|
13709
14169
|
try {
|
|
13710
|
-
|
|
14170
|
+
let report = normalizeWorkflowReport(payload);
|
|
13711
14171
|
if (report.error) {
|
|
13712
14172
|
json(res, 400, { error: report.error });
|
|
13713
14173
|
return;
|
|
@@ -13740,6 +14200,7 @@ export function startUiServer({
|
|
|
13740
14200
|
}
|
|
13741
14201
|
const scopedRoot = workflowScope.stateRoot;
|
|
13742
14202
|
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
|
|
14203
|
+
releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${tapdId}`);
|
|
13743
14204
|
const currentSnapshot = prdWorkflowMaterializeSnapshot(
|
|
13744
14205
|
workflowScope.executionRoot,
|
|
13745
14206
|
scopedRoot,
|
|
@@ -13747,26 +14208,28 @@ export function startUiServer({
|
|
|
13747
14208
|
userCtx,
|
|
13748
14209
|
{ flowSource, flowId },
|
|
13749
14210
|
);
|
|
13750
|
-
const
|
|
13751
|
-
String(currentSnapshot.runtimeRevision || "").trim(),
|
|
13752
|
-
String(currentSnapshot.revision || "").trim(),
|
|
13753
|
-
].filter(Boolean));
|
|
13754
|
-
if (report.expectedRevision && acceptedRevisions.size && !acceptedRevisions.has(report.expectedRevision)) {
|
|
13755
|
-
json(res, 409, {
|
|
13756
|
-
error: "Workflow state changed; refresh before reporting",
|
|
13757
|
-
conflict: {
|
|
13758
|
-
type: "workflow-revision-conflict",
|
|
13759
|
-
expectedRevision: report.expectedRevision,
|
|
13760
|
-
currentRevision: currentSnapshot.runtimeRevision || currentSnapshot.revision || "",
|
|
13761
|
-
workflow: report.workflow,
|
|
13762
|
-
},
|
|
13763
|
-
snapshot: currentSnapshot,
|
|
13764
|
-
});
|
|
13765
|
-
return;
|
|
13766
|
-
}
|
|
14211
|
+
const currentRuntimeRevision = String(currentSnapshot.runtimeRevision || "").trim();
|
|
13767
14212
|
if (report.idempotencyKey) {
|
|
13768
|
-
const existing = prdWorkflowFindCompletedIdempotencyEvent(
|
|
14213
|
+
const existing = prdWorkflowFindCompletedIdempotencyEvent(
|
|
14214
|
+
scopedRoot,
|
|
14215
|
+
tapdId,
|
|
14216
|
+
report.idempotencyKey,
|
|
14217
|
+
report.event.source,
|
|
14218
|
+
);
|
|
13769
14219
|
if (existing) {
|
|
14220
|
+
const existingFingerprint = prdWorkflowIdempotencyFingerprint(existing, report.idempotencyKey);
|
|
14221
|
+
if (existingFingerprint && existingFingerprint !== report.event.idempotencyFingerprint) {
|
|
14222
|
+
json(res, 409, {
|
|
14223
|
+
error: "Idempotency key was already used for a different Workflow report",
|
|
14224
|
+
conflict: {
|
|
14225
|
+
type: "workflow-idempotency-conflict",
|
|
14226
|
+
idempotencyKey: report.idempotencyKey,
|
|
14227
|
+
workflow: report.workflow,
|
|
14228
|
+
},
|
|
14229
|
+
snapshot: currentSnapshot,
|
|
14230
|
+
});
|
|
14231
|
+
return;
|
|
14232
|
+
}
|
|
13770
14233
|
json(res, 200, {
|
|
13771
14234
|
ok: true,
|
|
13772
14235
|
alreadyApplied: true,
|
|
@@ -13777,15 +14240,101 @@ export function startUiServer({
|
|
|
13777
14240
|
return;
|
|
13778
14241
|
}
|
|
13779
14242
|
}
|
|
13780
|
-
const
|
|
14243
|
+
const ownershipConflicts = prdWorkflowGlobalOwnershipConflicts(report, currentSnapshot);
|
|
14244
|
+
if (ownershipConflicts.length) {
|
|
14245
|
+
json(res, 409, {
|
|
14246
|
+
error: "Workflow globalState paths are owned by another report source",
|
|
14247
|
+
conflict: {
|
|
14248
|
+
type: "workflow-resource-ownership-conflict",
|
|
14249
|
+
conflicts: ownershipConflicts,
|
|
14250
|
+
workflow: report.workflow,
|
|
14251
|
+
},
|
|
14252
|
+
snapshot: currentSnapshot,
|
|
14253
|
+
});
|
|
14254
|
+
return;
|
|
14255
|
+
}
|
|
14256
|
+
const resourceKeys = workflowReportResourceKeys(report, currentSnapshot);
|
|
14257
|
+
const missingExpectedVersionKeys = Object.keys(report.expectedVersions).length
|
|
14258
|
+
? resourceKeys.filter((key) => !Object.prototype.hasOwnProperty.call(report.expectedVersions, key))
|
|
14259
|
+
: [];
|
|
14260
|
+
if (missingExpectedVersionKeys.length) {
|
|
14261
|
+
json(res, 400, {
|
|
14262
|
+
error: "expectedVersions must include every resource key touched by this report",
|
|
14263
|
+
missingExpectedVersionKeys,
|
|
14264
|
+
resourceKeys,
|
|
14265
|
+
});
|
|
14266
|
+
return;
|
|
14267
|
+
}
|
|
14268
|
+
const expectedTouchedVersions = Object.fromEntries(
|
|
14269
|
+
resourceKeys
|
|
14270
|
+
.filter((key) => Object.prototype.hasOwnProperty.call(report.expectedVersions, key))
|
|
14271
|
+
.map((key) => [key, report.expectedVersions[key]]),
|
|
14272
|
+
);
|
|
14273
|
+
const resourceConflicts = prdWorkflowResourceVersionConflicts(
|
|
14274
|
+
expectedTouchedVersions,
|
|
14275
|
+
currentSnapshot.resourceVersions || {},
|
|
14276
|
+
);
|
|
14277
|
+
if (resourceConflicts.length) {
|
|
14278
|
+
json(res, 409, {
|
|
14279
|
+
error: "Workflow resources changed; refresh the conflicting keys before reporting",
|
|
14280
|
+
conflict: {
|
|
14281
|
+
type: "workflow-resource-conflict",
|
|
14282
|
+
conflicts: resourceConflicts,
|
|
14283
|
+
workflow: report.workflow,
|
|
14284
|
+
},
|
|
14285
|
+
snapshot: currentSnapshot,
|
|
14286
|
+
});
|
|
14287
|
+
return;
|
|
14288
|
+
}
|
|
14289
|
+
if (!Object.keys(report.expectedVersions).length && report.expectedRevision && currentRuntimeRevision && report.expectedRevision !== currentRuntimeRevision) {
|
|
14290
|
+
json(res, 409, {
|
|
14291
|
+
error: "Workflow state changed; refresh before reporting",
|
|
14292
|
+
conflict: {
|
|
14293
|
+
type: "workflow-revision-conflict",
|
|
14294
|
+
expectedRevision: report.expectedRevision,
|
|
14295
|
+
currentRevision: currentRuntimeRevision,
|
|
14296
|
+
workflow: report.workflow,
|
|
14297
|
+
},
|
|
14298
|
+
snapshot: currentSnapshot,
|
|
14299
|
+
});
|
|
14300
|
+
return;
|
|
14301
|
+
}
|
|
14302
|
+
report = prdWorkflowMergeProducerTimeline(report, currentSnapshot);
|
|
14303
|
+
if (report.error) {
|
|
14304
|
+
json(res, 400, { error: report.error });
|
|
14305
|
+
return;
|
|
14306
|
+
}
|
|
14307
|
+
let observation = null;
|
|
14308
|
+
if (report.observation) {
|
|
14309
|
+
const observationPayload = {
|
|
14310
|
+
...payload,
|
|
14311
|
+
tapdId,
|
|
14312
|
+
clientId: report.observation.clientId || payload.clientId || payload.source || "workflow-reporter",
|
|
14313
|
+
observedAt: report.observation.observedAt || payload.observedAt || "",
|
|
14314
|
+
scope: report.observation.scope || payload.scope || "client",
|
|
14315
|
+
reportSource: report.event.source,
|
|
14316
|
+
};
|
|
14317
|
+
observation = prdWorkflowStoreClientObservation({
|
|
14318
|
+
scopedRoot,
|
|
14319
|
+
tapdId,
|
|
14320
|
+
rawState: report.observation.state,
|
|
14321
|
+
payload: observationPayload,
|
|
14322
|
+
req,
|
|
14323
|
+
userCtx,
|
|
14324
|
+
flowSource,
|
|
14325
|
+
flowId,
|
|
14326
|
+
});
|
|
14327
|
+
}
|
|
14328
|
+
const shouldStoreEvent = report.hasRuntimeUpdate || Boolean(report.idempotencyKey);
|
|
14329
|
+
const event = shouldStoreEvent ? prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
|
|
13781
14330
|
...report.event,
|
|
13782
14331
|
tapdId,
|
|
13783
14332
|
actor: {
|
|
13784
14333
|
userId: String(userCtx.userId || ""),
|
|
13785
14334
|
username: String(authUser.username || userCtx.userId || ""),
|
|
13786
14335
|
},
|
|
13787
|
-
});
|
|
13788
|
-
if (!event) throw new Error("Failed to store workflow report");
|
|
14336
|
+
}) : null;
|
|
14337
|
+
if (shouldStoreEvent && !event) throw new Error("Failed to store workflow report");
|
|
13789
14338
|
const snapshot = prdWorkflowWithAgentflowTokenDiagnostic(
|
|
13790
14339
|
prdWorkflowMaterializeSnapshot(
|
|
13791
14340
|
workflowScope.executionRoot,
|
|
@@ -13798,16 +14347,32 @@ export function startUiServer({
|
|
|
13798
14347
|
);
|
|
13799
14348
|
prdWorkflowBroadcast(
|
|
13800
14349
|
prdWorkflowKey(userCtx, flowSource, flowId, tapdId),
|
|
13801
|
-
{ type: "workflow-report", tapdId, workflow: report.workflow, event, snapshot },
|
|
14350
|
+
{ type: "workflow-report", tapdId, workflow: report.workflow, event, observation: Boolean(observation), snapshot },
|
|
13802
14351
|
);
|
|
13803
|
-
json(res, 200, {
|
|
14352
|
+
json(res, 200, {
|
|
14353
|
+
ok: true,
|
|
14354
|
+
report,
|
|
14355
|
+
resourceKeys,
|
|
14356
|
+
event,
|
|
14357
|
+
observation: observation ? {
|
|
14358
|
+
accepted: true,
|
|
14359
|
+
clientId: observation.reportMeta.clientId,
|
|
14360
|
+
observedAt: observation.reportMeta.observedAt,
|
|
14361
|
+
schema: report.observation.schema,
|
|
14362
|
+
} : null,
|
|
14363
|
+
snapshot,
|
|
14364
|
+
});
|
|
13804
14365
|
} catch (e) {
|
|
13805
14366
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
14367
|
+
} finally {
|
|
14368
|
+
releaseWorkflowWriteLock?.();
|
|
13806
14369
|
}
|
|
13807
14370
|
return;
|
|
13808
14371
|
}
|
|
13809
14372
|
|
|
13810
14373
|
if (req.method === "POST" && url.pathname === "/api/prd-workflow/event") {
|
|
14374
|
+
res.setHeader("Deprecation", "true");
|
|
14375
|
+
res.setHeader("Link", "</api/workflows/report>; rel=\"successor-version\"");
|
|
13811
14376
|
if (!authUser?.userId) {
|
|
13812
14377
|
json(res, 401, { error: "Authentication required" });
|
|
13813
14378
|
return;
|
|
@@ -13858,31 +14423,49 @@ export function startUiServer({
|
|
|
13858
14423
|
getSessionTokenFromRequest(req) || "",
|
|
13859
14424
|
);
|
|
13860
14425
|
prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "runtime-event", tapdId, event, snapshot });
|
|
13861
|
-
json(res, 200, {
|
|
14426
|
+
json(res, 200, {
|
|
14427
|
+
ok: true,
|
|
14428
|
+
event,
|
|
14429
|
+
snapshot,
|
|
14430
|
+
compatibility: {
|
|
14431
|
+
deprecatedEndpoint: "/api/prd-workflow/event",
|
|
14432
|
+
replacement: "/api/workflows/report with action/artifacts/extensions",
|
|
14433
|
+
},
|
|
14434
|
+
});
|
|
13862
14435
|
} catch (e) {
|
|
13863
14436
|
json(res, 500, { error: (e && e.message) || String(e) });
|
|
13864
14437
|
}
|
|
13865
14438
|
return;
|
|
13866
14439
|
}
|
|
13867
14440
|
|
|
13868
|
-
if (req.method === "POST" &&
|
|
14441
|
+
if (req.method === "POST" && (
|
|
14442
|
+
url.pathname === "/api/workflow-artifacts/publish" ||
|
|
14443
|
+
url.pathname === "/api/prd-workflow/review-link"
|
|
14444
|
+
)) {
|
|
14445
|
+
const legacyReviewEndpoint = url.pathname === "/api/prd-workflow/review-link";
|
|
13869
14446
|
if (!authUser?.userId) {
|
|
13870
14447
|
json(res, 401, { error: "Authentication required" });
|
|
13871
14448
|
return;
|
|
13872
14449
|
}
|
|
13873
14450
|
let payload;
|
|
13874
14451
|
try {
|
|
13875
|
-
payload = JSON.parse(await readBody(req));
|
|
13876
|
-
} catch {
|
|
13877
|
-
json(res, 400, { error: "Invalid JSON body" });
|
|
14452
|
+
payload = JSON.parse(await readBody(req, 600000));
|
|
14453
|
+
} catch (error) {
|
|
14454
|
+
json(res, error?.status === 413 ? 413 : 400, { error: error?.status === 413 ? error.message : "Invalid JSON body" });
|
|
13878
14455
|
return;
|
|
13879
14456
|
}
|
|
14457
|
+
let releaseWorkflowWriteLock = null;
|
|
13880
14458
|
try {
|
|
13881
|
-
const
|
|
13882
|
-
if (
|
|
13883
|
-
json(res, 400, { error:
|
|
14459
|
+
const workflow = normalizeWorkflowReference(payload);
|
|
14460
|
+
if (workflow.error) {
|
|
14461
|
+
json(res, 400, { error: workflow.error });
|
|
14462
|
+
return;
|
|
14463
|
+
}
|
|
14464
|
+
if (workflow.namespace !== "tapd") {
|
|
14465
|
+
json(res, 400, { error: `Unsupported workflow namespace: ${workflow.namespace}` });
|
|
13884
14466
|
return;
|
|
13885
14467
|
}
|
|
14468
|
+
const tapdId = workflow.id;
|
|
13886
14469
|
const flowId = String(payload.flowId || "").trim();
|
|
13887
14470
|
const flowSource = String(payload.flowSource || "user").trim() || "user";
|
|
13888
14471
|
const archived = payload.archived === true || payload.flowArchived === true;
|
|
@@ -13899,6 +14482,194 @@ export function startUiServer({
|
|
|
13899
14482
|
}
|
|
13900
14483
|
const scopedRoot = workflowScope.stateRoot;
|
|
13901
14484
|
prdWorkflowMigrateLegacyState(workflowScope.executionRoot, scopedRoot, tapdId);
|
|
14485
|
+
const producer = String(payload.source || (legacyReviewEndpoint ? "prd-flow" : "")).trim().toLowerCase();
|
|
14486
|
+
if (!producer) {
|
|
14487
|
+
json(res, 400, { error: "Workflow artifact publish requires source" });
|
|
14488
|
+
return;
|
|
14489
|
+
}
|
|
14490
|
+
if (!/^[a-z][a-z0-9._-]{0,119}$/.test(producer)) {
|
|
14491
|
+
json(res, 400, { error: "Invalid workflow report source" });
|
|
14492
|
+
return;
|
|
14493
|
+
}
|
|
14494
|
+
const fieldLimits = [
|
|
14495
|
+
[payload.title || payload.label, 160, "title"],
|
|
14496
|
+
[payload.stage || payload.stageKey || payload.stage_key, 240, "stage"],
|
|
14497
|
+
[payload.issueKey || payload.issue_key || payload.issue, 240, "issueKey"],
|
|
14498
|
+
[payload.platform, 80, "platform"],
|
|
14499
|
+
[payload.artifactLabel, 500, "artifactLabel"],
|
|
14500
|
+
[payload.reviewId || payload.review_id, 500, "reviewId"],
|
|
14501
|
+
];
|
|
14502
|
+
const oversizedField = fieldLimits.find(([value, max]) => String(value || "").trim().length > max);
|
|
14503
|
+
if (oversizedField) {
|
|
14504
|
+
json(res, 400, { error: `${oversizedField[2]} exceeds ${oversizedField[1]} characters` });
|
|
14505
|
+
return;
|
|
14506
|
+
}
|
|
14507
|
+
const markdown = String(payload.markdown || payload.content || payload.rawOutput || "");
|
|
14508
|
+
if (!markdown.trim()) {
|
|
14509
|
+
json(res, 400, { error: "Missing review markdown" });
|
|
14510
|
+
return;
|
|
14511
|
+
}
|
|
14512
|
+
if (Buffer.byteLength(markdown, "utf-8") > 500000) {
|
|
14513
|
+
json(res, 413, { error: "Review markdown exceeds 500000 bytes" });
|
|
14514
|
+
return;
|
|
14515
|
+
}
|
|
14516
|
+
const requestedDurability = String(
|
|
14517
|
+
payload.durability || (payload.durable === true || payload.permanent === true ? "durable" : "temporary"),
|
|
14518
|
+
).trim().toLowerCase() || "temporary";
|
|
14519
|
+
if (!["temporary", "durable"].includes(requestedDurability)) {
|
|
14520
|
+
json(res, 400, { error: "durability must be temporary or durable" });
|
|
14521
|
+
return;
|
|
14522
|
+
}
|
|
14523
|
+
const ttlInput = payload.ttlDays ?? payload.ttl_days;
|
|
14524
|
+
if (requestedDurability === "temporary" && ttlInput != null) {
|
|
14525
|
+
const ttlDays = Number(ttlInput);
|
|
14526
|
+
if (!Number.isInteger(ttlDays) || ttlDays < 1 || ttlDays > 30) {
|
|
14527
|
+
json(res, 400, { error: "ttlDays must be an integer between 1 and 30" });
|
|
14528
|
+
return;
|
|
14529
|
+
}
|
|
14530
|
+
}
|
|
14531
|
+
const explicitExpiresAt = String(payload.expiresAt || payload.expires_at || "").trim();
|
|
14532
|
+
if (explicitExpiresAt && (!Number.isFinite(Date.parse(explicitExpiresAt)) || Date.parse(explicitExpiresAt) <= Date.now())) {
|
|
14533
|
+
json(res, 400, { error: "expiresAt must be a valid future date" });
|
|
14534
|
+
return;
|
|
14535
|
+
}
|
|
14536
|
+
const idempotencyKey = String(
|
|
14537
|
+
payload.idempotencyKey || payload.idempotency_key || "",
|
|
14538
|
+
).trim();
|
|
14539
|
+
if (idempotencyKey.length > 500) {
|
|
14540
|
+
json(res, 400, { error: "idempotencyKey exceeds 500 characters" });
|
|
14541
|
+
return;
|
|
14542
|
+
}
|
|
14543
|
+
if (String(payload.artifactKey || payload.artifact_key || "").trim().length > 500) {
|
|
14544
|
+
json(res, 400, { error: "artifactKey exceeds 500 characters" });
|
|
14545
|
+
return;
|
|
14546
|
+
}
|
|
14547
|
+
const artifactKey = prdWorkflowReviewArtifactKey(tapdId, payload);
|
|
14548
|
+
const idempotencyFingerprint = prdWorkflowRevisionHash({
|
|
14549
|
+
operation: "artifact.publish",
|
|
14550
|
+
workflow,
|
|
14551
|
+
producer,
|
|
14552
|
+
title: String(payload.title || payload.label || "").trim(),
|
|
14553
|
+
markdown,
|
|
14554
|
+
stage: String(payload.stage || payload.stageKey || payload.stage_key || "").trim(),
|
|
14555
|
+
issueKey: String(payload.issueKey || payload.issue_key || payload.issue || "").trim(),
|
|
14556
|
+
platform: String(payload.platform || "").trim(),
|
|
14557
|
+
artifactKey,
|
|
14558
|
+
artifactLabel: String(payload.artifactLabel || "").trim(),
|
|
14559
|
+
durability: requestedDurability,
|
|
14560
|
+
ttlDays: ttlInput ?? null,
|
|
14561
|
+
expiresAt: explicitExpiresAt,
|
|
14562
|
+
});
|
|
14563
|
+
releaseWorkflowWriteLock = await prdWorkflowAcquireWriteLock(`${scopedRoot}\t${tapdId}`);
|
|
14564
|
+
const currentSnapshot = prdWorkflowMaterializeSnapshot(
|
|
14565
|
+
workflowScope.executionRoot,
|
|
14566
|
+
scopedRoot,
|
|
14567
|
+
tapdId,
|
|
14568
|
+
userCtx,
|
|
14569
|
+
{ flowSource, flowId },
|
|
14570
|
+
);
|
|
14571
|
+
const expectedRevision = String(payload.expectedRevision || payload.expected_revision || "").trim();
|
|
14572
|
+
if (expectedRevision.length > 500) {
|
|
14573
|
+
json(res, 400, { error: "expectedRevision exceeds 500 characters" });
|
|
14574
|
+
return;
|
|
14575
|
+
}
|
|
14576
|
+
const currentRuntimeRevision = String(currentSnapshot.runtimeRevision || "").trim();
|
|
14577
|
+
if (idempotencyKey) {
|
|
14578
|
+
const existing = prdWorkflowFindIdempotencyEvent(
|
|
14579
|
+
scopedRoot,
|
|
14580
|
+
tapdId,
|
|
14581
|
+
idempotencyKey,
|
|
14582
|
+
producer,
|
|
14583
|
+
false,
|
|
14584
|
+
"artifact.publish",
|
|
14585
|
+
);
|
|
14586
|
+
if (existing) {
|
|
14587
|
+
const existingFingerprint = prdWorkflowIdempotencyFingerprint(existing, idempotencyKey);
|
|
14588
|
+
if (existingFingerprint && existingFingerprint !== idempotencyFingerprint) {
|
|
14589
|
+
json(res, 409, {
|
|
14590
|
+
error: "Idempotency key was already used for different Artifact content",
|
|
14591
|
+
conflict: { type: "workflow-idempotency-conflict", idempotencyKey, workflow },
|
|
14592
|
+
snapshot: currentSnapshot,
|
|
14593
|
+
});
|
|
14594
|
+
return;
|
|
14595
|
+
}
|
|
14596
|
+
const artifact = Array.isArray(existing.artifacts) ? existing.artifacts[0] : null;
|
|
14597
|
+
json(res, 200, {
|
|
14598
|
+
ok: true,
|
|
14599
|
+
alreadyApplied: true,
|
|
14600
|
+
workflow,
|
|
14601
|
+
artifact,
|
|
14602
|
+
review: artifact ? {
|
|
14603
|
+
id: existing.reviewId || "",
|
|
14604
|
+
url: artifact.canonicalUrl || artifact.url || "",
|
|
14605
|
+
shortUrl: artifact.shortUrl || "",
|
|
14606
|
+
shortCode: existing.reviewShortCode || "",
|
|
14607
|
+
durability: existing.durability || artifact.durability || "",
|
|
14608
|
+
expiresAt: existing.expiresAt || artifact.expiresAt || "",
|
|
14609
|
+
} : null,
|
|
14610
|
+
event: existing,
|
|
14611
|
+
snapshot: currentSnapshot,
|
|
14612
|
+
});
|
|
14613
|
+
return;
|
|
14614
|
+
}
|
|
14615
|
+
}
|
|
14616
|
+
const resourceKey = `artifact:${producer}:${artifactKey}`;
|
|
14617
|
+
const hasExpectedVersionsField = Object.prototype.hasOwnProperty.call(payload, "expectedVersions")
|
|
14618
|
+
|| Object.prototype.hasOwnProperty.call(payload, "expected_versions");
|
|
14619
|
+
const rawExpectedVersionsInput = Object.prototype.hasOwnProperty.call(payload, "expectedVersions")
|
|
14620
|
+
? payload.expectedVersions
|
|
14621
|
+
: payload.expected_versions;
|
|
14622
|
+
if (hasExpectedVersionsField && (!rawExpectedVersionsInput || typeof rawExpectedVersionsInput !== "object" || Array.isArray(rawExpectedVersionsInput))) {
|
|
14623
|
+
json(res, 400, { error: "expectedVersions must be an object" });
|
|
14624
|
+
return;
|
|
14625
|
+
}
|
|
14626
|
+
const rawExpectedVersions = hasExpectedVersionsField ? rawExpectedVersionsInput : {};
|
|
14627
|
+
const invalidExpectedVersionEntry = Object.entries(rawExpectedVersions).find(([key, value]) => (
|
|
14628
|
+
!String(key || "").trim() || String(key).length > 800 || /[\0\r\n]/.test(String(key)) ||
|
|
14629
|
+
String(value == null || value === "" ? "absent" : value).trim().length > 160
|
|
14630
|
+
));
|
|
14631
|
+
if (invalidExpectedVersionEntry) {
|
|
14632
|
+
json(res, 400, { error: "expectedVersions contains an invalid resource key or version" });
|
|
14633
|
+
return;
|
|
14634
|
+
}
|
|
14635
|
+
if (Object.keys(rawExpectedVersions).length && !Object.prototype.hasOwnProperty.call(rawExpectedVersions, resourceKey)) {
|
|
14636
|
+
json(res, 400, {
|
|
14637
|
+
error: "expectedVersions must include the Artifact resource key touched by this publish",
|
|
14638
|
+
missingExpectedVersionKeys: [resourceKey],
|
|
14639
|
+
resourceKeys: [resourceKey],
|
|
14640
|
+
});
|
|
14641
|
+
return;
|
|
14642
|
+
}
|
|
14643
|
+
const expectedArtifactVersion = Object.prototype.hasOwnProperty.call(rawExpectedVersions, resourceKey)
|
|
14644
|
+
? String(rawExpectedVersions[resourceKey] || "absent")
|
|
14645
|
+
: null;
|
|
14646
|
+
const resourceConflicts = expectedArtifactVersion == null
|
|
14647
|
+
? []
|
|
14648
|
+
: prdWorkflowResourceVersionConflicts(
|
|
14649
|
+
{ [resourceKey]: expectedArtifactVersion },
|
|
14650
|
+
currentSnapshot.resourceVersions || {},
|
|
14651
|
+
);
|
|
14652
|
+
if (resourceConflicts.length) {
|
|
14653
|
+
json(res, 409, {
|
|
14654
|
+
error: "Workflow artifact changed; refresh before publishing",
|
|
14655
|
+
conflict: { type: "workflow-resource-conflict", conflicts: resourceConflicts, workflow },
|
|
14656
|
+
snapshot: currentSnapshot,
|
|
14657
|
+
});
|
|
14658
|
+
return;
|
|
14659
|
+
}
|
|
14660
|
+
if (!Object.keys(rawExpectedVersions).length && expectedRevision && currentRuntimeRevision && expectedRevision !== currentRuntimeRevision) {
|
|
14661
|
+
json(res, 409, {
|
|
14662
|
+
error: "Workflow state changed; refresh before publishing",
|
|
14663
|
+
conflict: {
|
|
14664
|
+
type: "workflow-revision-conflict",
|
|
14665
|
+
expectedRevision,
|
|
14666
|
+
currentRevision: currentRuntimeRevision,
|
|
14667
|
+
workflow,
|
|
14668
|
+
},
|
|
14669
|
+
snapshot: currentSnapshot,
|
|
14670
|
+
});
|
|
14671
|
+
return;
|
|
14672
|
+
}
|
|
13902
14673
|
const review = prdWorkflowCreateReview(
|
|
13903
14674
|
scopedRoot,
|
|
13904
14675
|
tapdId,
|
|
@@ -13920,7 +14691,6 @@ export function startUiServer({
|
|
|
13920
14691
|
const shortUrl = shortLink?.shortUrl || "";
|
|
13921
14692
|
const displayUrl = shortUrl || reviewUrl;
|
|
13922
14693
|
const durability = review.durability || "temporary";
|
|
13923
|
-
const artifactKey = prdWorkflowReviewArtifactKey(tapdId, payload);
|
|
13924
14694
|
const reviewStageKey = prdWorkflowRuntimeEventCanonicalStage(payload)
|
|
13925
14695
|
|| payload.stageKey
|
|
13926
14696
|
|| payload.stage_key
|
|
@@ -13932,9 +14702,6 @@ export function startUiServer({
|
|
|
13932
14702
|
const reviewSource = review.source && typeof review.source === "object" && !Array.isArray(review.source)
|
|
13933
14703
|
? review.source
|
|
13934
14704
|
: { kind: durability === "durable" ? "ai-doc" : "local-draft", durability };
|
|
13935
|
-
const idempotencyKey = String(
|
|
13936
|
-
payload.idempotencyKey || payload.idempotency_key || "",
|
|
13937
|
-
).trim();
|
|
13938
14705
|
const artifact = {
|
|
13939
14706
|
key: artifactKey,
|
|
13940
14707
|
label: payload.artifactLabel || "Markdown Review",
|
|
@@ -13950,6 +14717,7 @@ export function startUiServer({
|
|
|
13950
14717
|
issueKey: payload.issueKey || payload.issue_key || "",
|
|
13951
14718
|
platform: payload.platform || "",
|
|
13952
14719
|
stageKey: reviewStageKey,
|
|
14720
|
+
producer,
|
|
13953
14721
|
...(reviewMrUrl ? { mrUrl: reviewMrUrl } : {}),
|
|
13954
14722
|
...(reviewMrIid ? { mrIid: reviewMrIid } : {}),
|
|
13955
14723
|
...(reviewCommitSha ? { commitSha: reviewCommitSha } : {}),
|
|
@@ -13957,6 +14725,8 @@ export function startUiServer({
|
|
|
13957
14725
|
const event = prdWorkflowAppendRuntimeEvent(scopedRoot, tapdId, {
|
|
13958
14726
|
id: `review-link:${artifactKey}`,
|
|
13959
14727
|
type: "review-link",
|
|
14728
|
+
operation: "artifact.publish",
|
|
14729
|
+
source: producer,
|
|
13960
14730
|
auxiliary: true,
|
|
13961
14731
|
aggregateByStage: false,
|
|
13962
14732
|
conflictOnArtifact: false,
|
|
@@ -13974,6 +14744,8 @@ export function startUiServer({
|
|
|
13974
14744
|
...(reviewMrIid ? { mrIid: reviewMrIid } : {}),
|
|
13975
14745
|
...(reviewCommitSha ? { commitSha: reviewCommitSha } : {}),
|
|
13976
14746
|
idempotencyKey,
|
|
14747
|
+
idempotencyFingerprint,
|
|
14748
|
+
idempotencyFingerprints: idempotencyKey ? { [idempotencyKey]: idempotencyFingerprint } : {},
|
|
13977
14749
|
durability,
|
|
13978
14750
|
sourceArtifact: reviewSource,
|
|
13979
14751
|
expiresAt: review.expiresAt || "",
|
|
@@ -13988,6 +14760,7 @@ export function startUiServer({
|
|
|
13988
14760
|
persistence: "runtime",
|
|
13989
14761
|
durability,
|
|
13990
14762
|
source: reviewSource,
|
|
14763
|
+
producer,
|
|
13991
14764
|
expiresAt: review.expiresAt || "",
|
|
13992
14765
|
issueKey: artifact.issueKey,
|
|
13993
14766
|
platform: artifact.platform,
|
|
@@ -14004,8 +14777,15 @@ export function startUiServer({
|
|
|
14004
14777
|
getSessionTokenFromRequest(req) || "",
|
|
14005
14778
|
);
|
|
14006
14779
|
prdWorkflowBroadcast(prdWorkflowKey(userCtx, flowSource, flowId, tapdId), { type: "review-link", tapdId, event, snapshot });
|
|
14780
|
+
if (legacyReviewEndpoint) {
|
|
14781
|
+
res.setHeader("Deprecation", "true");
|
|
14782
|
+
res.setHeader("Link", "</api/workflow-artifacts/publish>; rel=\"successor-version\"");
|
|
14783
|
+
}
|
|
14007
14784
|
json(res, 200, {
|
|
14008
14785
|
ok: true,
|
|
14786
|
+
workflow,
|
|
14787
|
+
artifact,
|
|
14788
|
+
resourceKeys: [resourceKey],
|
|
14009
14789
|
review: {
|
|
14010
14790
|
...review,
|
|
14011
14791
|
url: reviewUrl,
|
|
@@ -14014,9 +14794,18 @@ export function startUiServer({
|
|
|
14014
14794
|
},
|
|
14015
14795
|
event,
|
|
14016
14796
|
snapshot,
|
|
14797
|
+
...(legacyReviewEndpoint ? {
|
|
14798
|
+
compatibility: {
|
|
14799
|
+
deprecatedEndpoint: "/api/prd-workflow/review-link",
|
|
14800
|
+
replacement: "/api/workflow-artifacts/publish",
|
|
14801
|
+
},
|
|
14802
|
+
} : {}),
|
|
14017
14803
|
});
|
|
14018
14804
|
} catch (e) {
|
|
14019
|
-
|
|
14805
|
+
const status = Number(e?.status);
|
|
14806
|
+
json(res, status >= 400 && status < 500 ? status : 500, { error: (e && e.message) || String(e) });
|
|
14807
|
+
} finally {
|
|
14808
|
+
releaseWorkflowWriteLock?.();
|
|
14020
14809
|
}
|
|
14021
14810
|
return;
|
|
14022
14811
|
}
|