lagora-cli 1.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +138 -0
- package/dist/help.txt +70 -0
- package/dist/lagora.js +342 -0
- package/dist/report-help.txt +5 -0
- package/dist/scripts/agora_playground_harness.py +263 -0
- package/dist/scripts/announce.js +41 -0
- package/dist/scripts/check-kernel-submission.py +90 -0
- package/dist/scripts/chunk-2EAJVB5D.js +100 -0
- package/dist/scripts/chunk-2KTLCUFI.js +29 -0
- package/dist/scripts/chunk-AZ3EEBVD.js +137 -0
- package/dist/scripts/chunk-NBJMYAOA.js +2128 -0
- package/dist/scripts/chunk-NCJMUBTG.js +125 -0
- package/dist/scripts/chunk-QJPQHKIO.js +23 -0
- package/dist/scripts/chunk-RIR5KGHC.js +33 -0
- package/dist/scripts/chunk-TJZVQYBL.js +8 -0
- package/dist/scripts/chunk-UHJXD4TG.js +18 -0
- package/dist/scripts/chunk-UQ6I6VTY.js +117 -0
- package/dist/scripts/cli-auth.js +348 -0
- package/dist/scripts/cli-config-IA7EOSYD.js +7 -0
- package/dist/scripts/install-skill.js +199 -0
- package/dist/scripts/issue-local-client-DZUXZOKY.js +22 -0
- package/dist/scripts/issue-search.js +1823 -0
- package/dist/scripts/issue.js +386 -0
- package/dist/scripts/keycloak-provision.js +986 -0
- package/dist/scripts/legato-fsim-runner.py +126 -0
- package/dist/scripts/legato-lowering-runner.py +156 -0
- package/dist/scripts/legato_runner_annotations.py +235 -0
- package/dist/scripts/legato_runner_env.py +91 -0
- package/dist/scripts/legato_runner_launchers.py +287 -0
- package/dist/scripts/legato_runner_script_wrapper.py +193 -0
- package/dist/scripts/notifications-EU43SIEV.js +624 -0
- package/dist/scripts/playground.js +408 -0
- package/dist/scripts/report-bundle-sync-3U7QTP4Z.js +215 -0
- package/dist/scripts/report.js +104 -0
- package/dist/scripts/resolve-sdk-package-version.py +151 -0
- package/dist/scripts/sdk-runtime-JE6H2PB2.js +992 -0
- package/dist/scripts/sdk-runtime-kubernetes-job-KOWL4ITV.js +479 -0
- package/dist/scripts/sdk-runtime-smoke.py +168 -0
- package/dist/scripts/sdk.js +256 -0
- package/dist/scripts/site-feedback-CAPE5MPX.js +136 -0
- package/dist/scripts/site-feedback-rate-limit-5BU2WSFE.js +86 -0
- package/dist/scripts/site-feedback.js +117 -0
- package/dist/scripts/storage-234FBH54.js +67 -0
- package/dist/scripts/submit-issue.sh +489 -0
- package/dist/scripts/verification-3QCY66QW.js +772 -0
- package/dist/scripts/verify-issue.js +144 -0
- package/dist/skills/legato-agora-cli/SKILL.md +556 -0
- package/dist/skills/legato-agora-cli/agents/openai.yaml +7 -0
- package/dist/skills/legato-agora-cli/reference/kernel-with-golden.py +84 -0
- package/dist/skills/legato-site-feedback/SKILL.md +49 -0
- package/dist/skills/legato-site-feedback/agents/openai.yaml +7 -0
- package/package.json +16 -0
|
@@ -0,0 +1,2128 @@
|
|
|
1
|
+
import {
|
|
2
|
+
normalizeVendorId
|
|
3
|
+
} from "./chunk-QJPQHKIO.js";
|
|
4
|
+
|
|
5
|
+
// lib/storage.ts
|
|
6
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
7
|
+
import { existsSync } from "node:fs";
|
|
8
|
+
import { mkdir as mkdir2, readdir, readFile as readFile2, rename, rm, stat, writeFile as writeFile2 } from "node:fs/promises";
|
|
9
|
+
import path2 from "node:path";
|
|
10
|
+
|
|
11
|
+
// lib/issue-assignment.ts
|
|
12
|
+
var IssueAssigneeName = class {
|
|
13
|
+
constructor(name) {
|
|
14
|
+
const trimmed = name.trim();
|
|
15
|
+
if (!trimmed) throw new Error("Assignee is required");
|
|
16
|
+
this.value = trimmed;
|
|
17
|
+
}
|
|
18
|
+
};
|
|
19
|
+
var IssueAssignment = class {
|
|
20
|
+
static create(input) {
|
|
21
|
+
return {
|
|
22
|
+
name: new IssueAssigneeName(input.assignee).value,
|
|
23
|
+
userId: input.assigneeUserId,
|
|
24
|
+
assignedBy: input.actor.trim() || "Language Developer",
|
|
25
|
+
assignedByUserId: input.actorUserId,
|
|
26
|
+
assignedByPrincipalSubject: input.actorPrincipalSubject,
|
|
27
|
+
assignedByPrincipalScope: input.actorPrincipalScope,
|
|
28
|
+
assignedAt: input.assignedAt
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
// lib/issue-blockers.ts
|
|
34
|
+
var DEFAULT_BLOCKER_REPOSITORY = "Hyper-Accel/hyperaccel-sdk";
|
|
35
|
+
var GithubRepository = class {
|
|
36
|
+
constructor(repository = DEFAULT_BLOCKER_REPOSITORY) {
|
|
37
|
+
const normalized = repository.trim().replace(/^https:\/\/github\.com\//, "").replace(/\.git$/, "") || DEFAULT_BLOCKER_REPOSITORY;
|
|
38
|
+
if (!/^[A-Za-z0-9_.-]+\/[A-Za-z0-9_.-]+$/.test(normalized)) {
|
|
39
|
+
throw new Error(`Unsupported GitHub repository: ${repository}`);
|
|
40
|
+
}
|
|
41
|
+
this.value = normalized;
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
var IssueBlockerTags = class _IssueBlockerTags {
|
|
45
|
+
static merge(tags, blockers) {
|
|
46
|
+
const activeTags = blockers.length ? blockers.map((blocker) => _IssueBlockerTags.forStatus(blocker.status)) : [];
|
|
47
|
+
return uniqueTags([..._IssueBlockerTags.withoutBlockerTags(tags), ...activeTags]);
|
|
48
|
+
}
|
|
49
|
+
static forStatus(status) {
|
|
50
|
+
if (status === "merged") return "blocker:merged";
|
|
51
|
+
if (status === "closed") return "blocker:closed";
|
|
52
|
+
return "blocker:pr";
|
|
53
|
+
}
|
|
54
|
+
static withoutBlockerTags(tags) {
|
|
55
|
+
return uniqueTags(tags).filter((tag) => !tag.startsWith("blocker:"));
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
var GithubPullRequestSyncService = class {
|
|
59
|
+
async sync(blocker) {
|
|
60
|
+
const syncedAt = (/* @__PURE__ */ new Date()).toISOString();
|
|
61
|
+
try {
|
|
62
|
+
const response = await fetch(`https://api.github.com/repos/${blocker.repository}/pulls/${blocker.number}`, {
|
|
63
|
+
headers: this.headers()
|
|
64
|
+
});
|
|
65
|
+
if (!response.ok) {
|
|
66
|
+
return { ...blocker, status: "unknown", lastSyncedAt: syncedAt, syncError: `HTTP ${response.status}` };
|
|
67
|
+
}
|
|
68
|
+
const payload = await response.json();
|
|
69
|
+
if (!isGithubPullRequestPayload(payload)) {
|
|
70
|
+
return { ...blocker, status: "unknown", lastSyncedAt: syncedAt, syncError: "Unsupported GitHub response" };
|
|
71
|
+
}
|
|
72
|
+
return {
|
|
73
|
+
...blocker,
|
|
74
|
+
title: payload.title,
|
|
75
|
+
url: payload.html_url ?? blocker.url,
|
|
76
|
+
status: githubPrStatus(payload),
|
|
77
|
+
lastSyncedAt: syncedAt,
|
|
78
|
+
syncError: void 0
|
|
79
|
+
};
|
|
80
|
+
} catch (error) {
|
|
81
|
+
if (error instanceof Error) {
|
|
82
|
+
return { ...blocker, status: "unknown", lastSyncedAt: syncedAt, syncError: error.message };
|
|
83
|
+
}
|
|
84
|
+
throw error;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
headers() {
|
|
88
|
+
const token = process.env.AGORA_GITHUB_TOKEN?.trim();
|
|
89
|
+
return {
|
|
90
|
+
Accept: "application/vnd.github+json",
|
|
91
|
+
"User-Agent": "legato-dev-agora",
|
|
92
|
+
...token ? { Authorization: `Bearer ${token}` } : {}
|
|
93
|
+
};
|
|
94
|
+
}
|
|
95
|
+
};
|
|
96
|
+
function githubPullRequestUrl(repository, number) {
|
|
97
|
+
return `https://github.com/${repository}/pull/${number}`;
|
|
98
|
+
}
|
|
99
|
+
function uniqueTags(tags) {
|
|
100
|
+
return [...new Set(tags.map((tag) => tag.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9:_./-]/g, "")).filter(Boolean))];
|
|
101
|
+
}
|
|
102
|
+
function isGithubPullRequestPayload(value) {
|
|
103
|
+
if (!value || typeof value !== "object" || Array.isArray(value)) return false;
|
|
104
|
+
const state = Reflect.get(value, "state");
|
|
105
|
+
return state === void 0 || typeof state === "string";
|
|
106
|
+
}
|
|
107
|
+
function githubPrStatus(payload) {
|
|
108
|
+
if (payload.merged_at) return "merged";
|
|
109
|
+
if (payload.state === "open") return "open";
|
|
110
|
+
if (payload.state === "closed") return "closed";
|
|
111
|
+
return "unknown";
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// lib/server/storage-notifications.ts
|
|
115
|
+
async function notifyIssueCreatedBestEffort(issue, actorUserId) {
|
|
116
|
+
try {
|
|
117
|
+
const { createIssueCreatedNotifications } = await import("./notifications-EU43SIEV.js");
|
|
118
|
+
await createIssueCreatedNotifications(issue, actorUserId);
|
|
119
|
+
} catch (error) {
|
|
120
|
+
reportNotificationError(error);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
async function notifyIssueChangedBestEffort(issue, actorUserId, eventType) {
|
|
124
|
+
try {
|
|
125
|
+
const { createIssueChangeNotifications } = await import("./notifications-EU43SIEV.js");
|
|
126
|
+
await createIssueChangeNotifications({ issue, actorUserId, eventType });
|
|
127
|
+
} catch (error) {
|
|
128
|
+
reportNotificationError(error);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
async function notifyIssueDeletedBestEffort(issue, actorUserId) {
|
|
132
|
+
try {
|
|
133
|
+
const { createIssueDeletedNotifications } = await import("./notifications-EU43SIEV.js");
|
|
134
|
+
await createIssueDeletedNotifications(issue, actorUserId);
|
|
135
|
+
} catch (error) {
|
|
136
|
+
reportNotificationError(error);
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
async function notifyVerificationChangedBestEffort(issue, run, actorUserId) {
|
|
140
|
+
const eventType = verificationNotificationEventType(run.status);
|
|
141
|
+
if (!eventType) return;
|
|
142
|
+
await notifyIssueChangedBestEffort(issue, actorUserId, eventType);
|
|
143
|
+
}
|
|
144
|
+
async function notifyVerificationDeletedBestEffort(issue, actorUserId) {
|
|
145
|
+
await notifyIssueChangedBestEffort(issue, actorUserId, "verification_deleted");
|
|
146
|
+
}
|
|
147
|
+
function verificationNotificationEventType(status) {
|
|
148
|
+
if (status === "success") return "verification_succeeded";
|
|
149
|
+
if (status === "failed") return "verification_failed";
|
|
150
|
+
if (status === "skipped") return "verification_skipped";
|
|
151
|
+
return void 0;
|
|
152
|
+
}
|
|
153
|
+
function reportNotificationError(error) {
|
|
154
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
155
|
+
console.warn(`Issue notification skipped: ${message}`);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// lib/verification-run-staleness.ts
|
|
159
|
+
var DEFAULT_VERIFICATION_RUN_STALE_MS = 9e5;
|
|
160
|
+
function verificationRunStaleMs() {
|
|
161
|
+
const parsed = Number.parseInt(process.env.AGORA_VERIFICATION_RUN_STALE_MS ?? "", 10);
|
|
162
|
+
return Number.isFinite(parsed) && parsed > 0 ? parsed : DEFAULT_VERIFICATION_RUN_STALE_MS;
|
|
163
|
+
}
|
|
164
|
+
function isVerificationRunActive(run, staleMs, nowMs) {
|
|
165
|
+
if (run.status !== "running") return false;
|
|
166
|
+
const lastActivity = Date.parse(run.completedAt ?? run.startedAt ?? "");
|
|
167
|
+
if (!Number.isFinite(lastActivity)) return false;
|
|
168
|
+
return nowMs - lastActivity < staleMs;
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// lib/verification-generated-artifact-names.ts
|
|
172
|
+
function storedGeneratedArtifactFilename(relativePath) {
|
|
173
|
+
const parsed = parseGeneratedArtifactPath(relativePath);
|
|
174
|
+
const originalName = parsed.filename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
175
|
+
return `lowering-${parsed.stageLabel}-${originalName}`;
|
|
176
|
+
}
|
|
177
|
+
function generatedArtifactFilenameCandidates(relativePath) {
|
|
178
|
+
return [
|
|
179
|
+
storedGeneratedArtifactFilename(relativePath),
|
|
180
|
+
`lowering-${relativePath.replace(/[/\\]+/g, "-").replace(/[^a-zA-Z0-9._-]/g, "_")}`
|
|
181
|
+
];
|
|
182
|
+
}
|
|
183
|
+
function parseGeneratedArtifactPath(filenameOrPath) {
|
|
184
|
+
const parts = filenameOrPath.split(/[\\/]+/).filter(Boolean);
|
|
185
|
+
const first = parts[0]?.toLowerCase();
|
|
186
|
+
const stageLabel = stageLabelForSegment(first);
|
|
187
|
+
return {
|
|
188
|
+
stageLabel,
|
|
189
|
+
filename: parts.at(-1) ?? filenameOrPath
|
|
190
|
+
};
|
|
191
|
+
}
|
|
192
|
+
function stageLabelForSegment(segment) {
|
|
193
|
+
if (segment === "mlir") return "MLIR";
|
|
194
|
+
if (segment === "core_ir") return "CORE_IR";
|
|
195
|
+
if (segment === "backend_ir") return "BACKEND_IR";
|
|
196
|
+
if (segment === "asm") return "ASM";
|
|
197
|
+
if (segment === "binary") return "BINARY";
|
|
198
|
+
if (segment === "fsim") return "FSIM";
|
|
199
|
+
return "OUTPUT";
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
// lib/server/automation-settings.ts
|
|
203
|
+
import { mkdir, readFile, writeFile } from "node:fs/promises";
|
|
204
|
+
import path from "node:path";
|
|
205
|
+
async function readAutomationSettings() {
|
|
206
|
+
const defaults = defaultAutomationSettings();
|
|
207
|
+
try {
|
|
208
|
+
const raw = await readFile(automationSettingsPath(), "utf8");
|
|
209
|
+
const parsed = JSON.parse(raw);
|
|
210
|
+
if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) return defaults;
|
|
211
|
+
const autoLoweringEnabled = Reflect.get(parsed, "autoLoweringEnabled");
|
|
212
|
+
const updatedAt = Reflect.get(parsed, "updatedAt");
|
|
213
|
+
return {
|
|
214
|
+
autoLoweringEnabled: typeof autoLoweringEnabled === "boolean" ? autoLoweringEnabled : defaults.autoLoweringEnabled,
|
|
215
|
+
updatedAt: typeof updatedAt === "string" && updatedAt.trim() ? updatedAt.trim() : void 0
|
|
216
|
+
};
|
|
217
|
+
} catch {
|
|
218
|
+
return defaults;
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
async function writeAutomationSettings(input) {
|
|
222
|
+
const settings = {
|
|
223
|
+
autoLoweringEnabled: input.autoLoweringEnabled,
|
|
224
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
225
|
+
};
|
|
226
|
+
await mkdir(automationSettingsRoot(), { recursive: true });
|
|
227
|
+
await writeFile(automationSettingsPath(), `${JSON.stringify(settings, null, 2)}
|
|
228
|
+
`);
|
|
229
|
+
return settings;
|
|
230
|
+
}
|
|
231
|
+
async function isAutoLoweringEnabled() {
|
|
232
|
+
return (await readAutomationSettings()).autoLoweringEnabled;
|
|
233
|
+
}
|
|
234
|
+
function defaultAutomationSettings() {
|
|
235
|
+
return {
|
|
236
|
+
autoLoweringEnabled: readBooleanOverride(process.env.AGORA_AUTO_LOWERING) ?? true
|
|
237
|
+
};
|
|
238
|
+
}
|
|
239
|
+
function readBooleanOverride(value) {
|
|
240
|
+
if (value === "1" || value === "true") return true;
|
|
241
|
+
if (value === "0" || value === "false") return false;
|
|
242
|
+
return void 0;
|
|
243
|
+
}
|
|
244
|
+
function automationSettingsRoot() {
|
|
245
|
+
return process.env.AGORA_SETTINGS_DIR ? path.resolve(process.env.AGORA_SETTINGS_DIR) : path.join(
|
|
246
|
+
/*turbopackIgnore: true*/
|
|
247
|
+
process.cwd(),
|
|
248
|
+
".local-data",
|
|
249
|
+
"settings"
|
|
250
|
+
);
|
|
251
|
+
}
|
|
252
|
+
function automationSettingsPath() {
|
|
253
|
+
return path.join(automationSettingsRoot(), "automation.json");
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
// lib/storage.ts
|
|
257
|
+
var KNOWN_AREA_TAGS = ["lowering", "fsim", "compile", "runtime", "diagnostics"];
|
|
258
|
+
var dataRoot = path2.join(
|
|
259
|
+
/*turbopackIgnore: true*/
|
|
260
|
+
process.cwd(),
|
|
261
|
+
".local-data"
|
|
262
|
+
);
|
|
263
|
+
var artifactsRoot = path2.join(dataRoot, "artifacts");
|
|
264
|
+
var dbPath = path2.join(dataRoot, "db.json");
|
|
265
|
+
var hasExplicitIssueStore = Boolean(process.env.AGORA_ISSUE_STORE);
|
|
266
|
+
var hasIsolatedIssueStore = hasExplicitIssueStore && process.env.AGORA_ISSUE_STORE_MODE === "isolated";
|
|
267
|
+
var issueStoreRoot = process.env.AGORA_ISSUE_STORE ? path2.resolve(process.env.AGORA_ISSUE_STORE) : path2.join(
|
|
268
|
+
/*turbopackIgnore: true*/
|
|
269
|
+
process.cwd(),
|
|
270
|
+
"issue-store"
|
|
271
|
+
);
|
|
272
|
+
var bundledIssueStoreRoot = path2.join(
|
|
273
|
+
/*turbopackIgnore: true*/
|
|
274
|
+
process.cwd(),
|
|
275
|
+
"issue-store"
|
|
276
|
+
);
|
|
277
|
+
var tagRegistryPath = path2.join(hasIsolatedIssueStore ? issueStoreRoot : dataRoot, "tags.json");
|
|
278
|
+
var emptyDb = () => ({
|
|
279
|
+
issues: [],
|
|
280
|
+
bundles: [],
|
|
281
|
+
artifacts: [],
|
|
282
|
+
comments: [],
|
|
283
|
+
verificationRuns: [],
|
|
284
|
+
occurrences: []
|
|
285
|
+
});
|
|
286
|
+
async function writeTextAtomic(filePath, content) {
|
|
287
|
+
const tempPath = `${filePath}.${process.pid}.${randomUUID()}.tmp`;
|
|
288
|
+
await writeFile2(tempPath, content);
|
|
289
|
+
await rename(tempPath, filePath);
|
|
290
|
+
}
|
|
291
|
+
async function writePortableIssueJson(issuePath, portable) {
|
|
292
|
+
await writeTextAtomic(issuePath, JSON.stringify(portable, null, 2));
|
|
293
|
+
}
|
|
294
|
+
var defaultTagRegistry = [
|
|
295
|
+
{ id: "area:lowering", label: "Lowering", description: "Lowering pipeline issue", color: "red", system: true },
|
|
296
|
+
{ id: "area:fsim", label: "fSIM", description: "Functional simulation or verification issue", color: "orange", system: true },
|
|
297
|
+
{ id: "area:compile", label: "Compile", description: "Compilation or build issue", color: "orange", system: true },
|
|
298
|
+
{ id: "area:runtime", label: "Runtime", description: "Runtime behavior issue", color: "orange", system: true },
|
|
299
|
+
{ id: "area:diagnostics", label: "Diagnostics", description: "Error message or diagnostic quality issue", color: "blue", system: true },
|
|
300
|
+
{ id: "regression", label: "Regression", description: "Previously working behavior regressed", color: "red" },
|
|
301
|
+
{ id: "needs-repro", label: "Needs Repro", description: "More reproduction details are needed", color: "gray" },
|
|
302
|
+
{ id: "question", label: "Question", description: "Needs language-team clarification", color: "blue" }
|
|
303
|
+
];
|
|
304
|
+
async function ensureStore() {
|
|
305
|
+
await mkdir2(hasIsolatedIssueStore ? issueStoreRoot : artifactsRoot, { recursive: true });
|
|
306
|
+
try {
|
|
307
|
+
await readFile2(tagRegistryPath, "utf8");
|
|
308
|
+
} catch {
|
|
309
|
+
await writeFile2(tagRegistryPath, JSON.stringify(defaultTagRegistry, null, 2));
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
var dbRawCache;
|
|
313
|
+
async function readDbRaw() {
|
|
314
|
+
try {
|
|
315
|
+
const details = await stat(dbPath);
|
|
316
|
+
if (dbRawCache && dbRawCache.mtimeMs === details.mtimeMs && dbRawCache.size === details.size) {
|
|
317
|
+
return dbRawCache.raw;
|
|
318
|
+
}
|
|
319
|
+
const raw = await readFile2(dbPath, "utf8");
|
|
320
|
+
dbRawCache = { mtimeMs: details.mtimeMs, size: details.size, raw };
|
|
321
|
+
return raw;
|
|
322
|
+
} catch {
|
|
323
|
+
const raw = JSON.stringify(emptyDb(), null, 2);
|
|
324
|
+
await writeTextAtomic(dbPath, raw);
|
|
325
|
+
try {
|
|
326
|
+
const details = await stat(dbPath);
|
|
327
|
+
dbRawCache = { mtimeMs: details.mtimeMs, size: details.size, raw };
|
|
328
|
+
} catch {
|
|
329
|
+
dbRawCache = void 0;
|
|
330
|
+
}
|
|
331
|
+
return raw;
|
|
332
|
+
}
|
|
333
|
+
}
|
|
334
|
+
async function readDb() {
|
|
335
|
+
if (hasIsolatedIssueStore) return emptyDb();
|
|
336
|
+
await ensureStore();
|
|
337
|
+
const raw = await readDbRaw();
|
|
338
|
+
const parsed = JSON.parse(raw);
|
|
339
|
+
return {
|
|
340
|
+
issues: parsed.issues ?? [],
|
|
341
|
+
bundles: parsed.bundles ?? [],
|
|
342
|
+
artifacts: parsed.artifacts ?? [],
|
|
343
|
+
comments: parsed.comments ?? [],
|
|
344
|
+
verificationRuns: parsed.verificationRuns ?? [],
|
|
345
|
+
occurrences: parsed.occurrences ?? []
|
|
346
|
+
};
|
|
347
|
+
}
|
|
348
|
+
async function writeDb(db) {
|
|
349
|
+
await ensureStore();
|
|
350
|
+
const raw = JSON.stringify(db, null, 2);
|
|
351
|
+
await writeTextAtomic(dbPath, raw);
|
|
352
|
+
try {
|
|
353
|
+
const details = await stat(dbPath);
|
|
354
|
+
dbRawCache = { mtimeMs: details.mtimeMs, size: details.size, raw };
|
|
355
|
+
} catch {
|
|
356
|
+
dbRawCache = void 0;
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
function now() {
|
|
360
|
+
return (/* @__PURE__ */ new Date()).toISOString();
|
|
361
|
+
}
|
|
362
|
+
function sanitizeFilename(filename) {
|
|
363
|
+
return filename.replace(/[^a-zA-Z0-9._-]/g, "_");
|
|
364
|
+
}
|
|
365
|
+
function assertSafeIssueId(id) {
|
|
366
|
+
if (!/^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(id)) {
|
|
367
|
+
throw new Error(`Unsafe issue id: ${id}`);
|
|
368
|
+
}
|
|
369
|
+
}
|
|
370
|
+
function assertSafeFilename(filename) {
|
|
371
|
+
if (!filename || filename === "." || filename === ".." || filename !== path2.basename(filename)) {
|
|
372
|
+
throw new Error(`Unsafe artifact filename: ${filename}`);
|
|
373
|
+
}
|
|
374
|
+
}
|
|
375
|
+
function safeJoin(root, ...segments) {
|
|
376
|
+
const resolvedRoot = path2.resolve(root);
|
|
377
|
+
const target = path2.resolve(resolvedRoot, ...segments);
|
|
378
|
+
if (target !== resolvedRoot && !target.startsWith(`${resolvedRoot}${path2.sep}`)) {
|
|
379
|
+
throw new Error(`Path escapes storage root: ${segments.join("/")}`);
|
|
380
|
+
}
|
|
381
|
+
return target;
|
|
382
|
+
}
|
|
383
|
+
function safeArtifactPath(root, relativePath) {
|
|
384
|
+
if (path2.isAbsolute(relativePath)) {
|
|
385
|
+
throw new Error(`Absolute artifact path is not allowed: ${relativePath}`);
|
|
386
|
+
}
|
|
387
|
+
return safeJoin(root, relativePath);
|
|
388
|
+
}
|
|
389
|
+
function normalizeTag(tag) {
|
|
390
|
+
return tag.trim().toLowerCase().replace(/\s+/g, "-").replace(/[^a-z0-9:_./-]/g, "");
|
|
391
|
+
}
|
|
392
|
+
function uniqueTags2(tags) {
|
|
393
|
+
return [...new Set(tags.map(normalizeTag).filter(Boolean))];
|
|
394
|
+
}
|
|
395
|
+
function issueAssignees(issue) {
|
|
396
|
+
const merged = [...issue.assignees ?? [], ...issue.assignee ? [issue.assignee] : []];
|
|
397
|
+
const byName = /* @__PURE__ */ new Map();
|
|
398
|
+
for (const assignee of merged) {
|
|
399
|
+
if (assignee?.name.trim()) byName.set(assignee.name.trim().toLowerCase(), assignee);
|
|
400
|
+
}
|
|
401
|
+
return [...byName.values()].filter(Boolean);
|
|
402
|
+
}
|
|
403
|
+
function firstAssignee(assignees) {
|
|
404
|
+
return assignees[0];
|
|
405
|
+
}
|
|
406
|
+
function sameOptionalId(left, right) {
|
|
407
|
+
return (left ?? "") === (right ?? "");
|
|
408
|
+
}
|
|
409
|
+
function normalizeSignatureText(text) {
|
|
410
|
+
return text.toLowerCase().replace(/[a-f0-9]{16,}/g, "<hash>").replace(/0x[a-f0-9]+/g, "<addr>").replace(/\d+/g, "<num>").replace(/\s+/g, " ").trim();
|
|
411
|
+
}
|
|
412
|
+
function rawFingerprint(input) {
|
|
413
|
+
const diagnosticSignature = normalizeSignatureText(`${input.title}
|
|
414
|
+
${input.description}`).slice(0, 240);
|
|
415
|
+
const key = createHash("sha256").update(["raw", input.kernelChecksum, input.logChecksum ?? "no-log", diagnosticSignature].join("\0")).digest("hex");
|
|
416
|
+
return { key, basis: "raw", artifactHash: input.kernelChecksum, diagnosticSignature, createdAt: input.createdAt };
|
|
417
|
+
}
|
|
418
|
+
function upsertFingerprint(fingerprints, fingerprint) {
|
|
419
|
+
const existingIndex = fingerprints.findIndex((item) => item.key === fingerprint.key);
|
|
420
|
+
if (existingIndex >= 0) {
|
|
421
|
+
return fingerprints.map((item, index) => index === existingIndex ? fingerprint : item);
|
|
422
|
+
}
|
|
423
|
+
return [fingerprint, ...fingerprints];
|
|
424
|
+
}
|
|
425
|
+
function makeOccurrence(input) {
|
|
426
|
+
return {
|
|
427
|
+
id: randomUUID(),
|
|
428
|
+
issueId: input.issueId,
|
|
429
|
+
reporter: input.reporter,
|
|
430
|
+
reporterUserId: input.reporterUserId,
|
|
431
|
+
reporterPrincipalSubject: input.reporterPrincipalSubject,
|
|
432
|
+
reporterPrincipalScope: input.reporterPrincipalScope,
|
|
433
|
+
reporterVendorId: input.reporterVendorId,
|
|
434
|
+
bundleId: input.bundle.id,
|
|
435
|
+
artifactIds: input.artifacts.map((artifact) => artifact.id),
|
|
436
|
+
kernelChecksum: input.kernelChecksum,
|
|
437
|
+
source: input.bundle.source,
|
|
438
|
+
gitMetadata: input.bundle.gitMetadata,
|
|
439
|
+
environmentMetadata: input.bundle.environmentMetadata,
|
|
440
|
+
createdAt: input.createdAt
|
|
441
|
+
};
|
|
442
|
+
}
|
|
443
|
+
async function listTagDefinitions() {
|
|
444
|
+
await ensureStore();
|
|
445
|
+
const raw = await readFile2(tagRegistryPath, "utf8");
|
|
446
|
+
const tags = JSON.parse(raw);
|
|
447
|
+
return tags.map((tag) => ({ ...tag, id: normalizeTag(tag.id) })).filter((tag) => tag.id && tag.label);
|
|
448
|
+
}
|
|
449
|
+
async function addTagDefinition(input) {
|
|
450
|
+
const tags = await listTagDefinitions();
|
|
451
|
+
const id = normalizeTag(input.id);
|
|
452
|
+
if (!id || tags.some((tag) => tag.id === id)) return;
|
|
453
|
+
tags.push({
|
|
454
|
+
id,
|
|
455
|
+
label: input.label.trim() || id,
|
|
456
|
+
description: input.description?.trim() || void 0,
|
|
457
|
+
color: input.color ?? "blue"
|
|
458
|
+
});
|
|
459
|
+
await writeFile2(tagRegistryPath, JSON.stringify(tags, null, 2));
|
|
460
|
+
}
|
|
461
|
+
async function deleteTagDefinition(id) {
|
|
462
|
+
const normalizedId = normalizeTag(id);
|
|
463
|
+
const tags = await listTagDefinitions();
|
|
464
|
+
const target = tags.find((tag) => tag.id === normalizedId);
|
|
465
|
+
if (!target || target.system) return;
|
|
466
|
+
await writeFile2(tagRegistryPath, JSON.stringify(tags.filter((tag) => tag.id !== normalizedId), null, 2));
|
|
467
|
+
}
|
|
468
|
+
function deriveTags(input) {
|
|
469
|
+
const text = `${input.title}
|
|
470
|
+
${input.description}`.toLowerCase();
|
|
471
|
+
const areaTags = KNOWN_AREA_TAGS.filter((tag) => text.includes(tag)).map((tag) => `area:${tag}`);
|
|
472
|
+
return uniqueTags2([
|
|
473
|
+
...areaTags,
|
|
474
|
+
input.kernelFilename ? `kernel:${input.kernelFilename}` : "",
|
|
475
|
+
input.hasLog ? "has-log" : "no-log",
|
|
476
|
+
input.source ? `source:${input.source}` : "",
|
|
477
|
+
...input.tags ?? []
|
|
478
|
+
]);
|
|
479
|
+
}
|
|
480
|
+
async function storeFile(file, kind) {
|
|
481
|
+
const bytes = Buffer.from(await file.arrayBuffer());
|
|
482
|
+
const id = randomUUID();
|
|
483
|
+
const checksum = createHash("sha256").update(bytes).digest("hex");
|
|
484
|
+
const filename = sanitizeFilename(file.name || `${kind}.txt`);
|
|
485
|
+
const relativePath = path2.posix.join("artifacts", `${id}-${filename}`);
|
|
486
|
+
await writeFile2(safeArtifactPath(dataRoot, relativePath), bytes);
|
|
487
|
+
return {
|
|
488
|
+
id,
|
|
489
|
+
kind,
|
|
490
|
+
filename,
|
|
491
|
+
contentType: file.type || "text/plain",
|
|
492
|
+
size: bytes.byteLength,
|
|
493
|
+
checksum,
|
|
494
|
+
relativePath,
|
|
495
|
+
createdAt: now()
|
|
496
|
+
};
|
|
497
|
+
}
|
|
498
|
+
async function createIssue(input) {
|
|
499
|
+
if (hasIsolatedIssueStore) throw new Error("Use lagora report --store to create a portable issue");
|
|
500
|
+
const db = await readDb();
|
|
501
|
+
const createdAt = now();
|
|
502
|
+
const creatorVendorId = normalizeVendorId(input.creatorVendorId);
|
|
503
|
+
const kernelArtifact = await storeFile(input.kernelFile, "kernel");
|
|
504
|
+
const artifacts = [kernelArtifact];
|
|
505
|
+
let errorLogArtifact;
|
|
506
|
+
if (input.errorLogFile && input.errorLogFile.size > 0) {
|
|
507
|
+
errorLogArtifact = await storeFile(input.errorLogFile, "error-log");
|
|
508
|
+
artifacts.push(errorLogArtifact);
|
|
509
|
+
}
|
|
510
|
+
const bundle = {
|
|
511
|
+
id: randomUUID(),
|
|
512
|
+
source: input.source ?? "web",
|
|
513
|
+
kernelArtifactId: kernelArtifact.id,
|
|
514
|
+
errorLogArtifactId: errorLogArtifact?.id,
|
|
515
|
+
legatoBinaryVersion: input.legatoBinaryVersion,
|
|
516
|
+
cliVersion: input.cliVersion,
|
|
517
|
+
gitMetadata: input.gitMetadata,
|
|
518
|
+
environmentMetadata: input.environmentMetadata,
|
|
519
|
+
createdAt
|
|
520
|
+
};
|
|
521
|
+
const fingerprint = rawFingerprint({
|
|
522
|
+
kernelChecksum: kernelArtifact.checksum,
|
|
523
|
+
logChecksum: errorLogArtifact?.checksum,
|
|
524
|
+
title: input.title,
|
|
525
|
+
description: input.description,
|
|
526
|
+
createdAt
|
|
527
|
+
});
|
|
528
|
+
const duplicate = db.issues.find((item) => normalizeVendorId(item.creatorVendorId) === creatorVendorId && (item.fingerprint?.key === fingerprint.key || (item.fingerprints ?? []).some((existing) => existing.key === fingerprint.key)));
|
|
529
|
+
if (duplicate) {
|
|
530
|
+
const occurrence2 = makeOccurrence({
|
|
531
|
+
issueId: duplicate.id,
|
|
532
|
+
reporter: input.creator.trim() || "Kernel Developer",
|
|
533
|
+
reporterUserId: input.creatorUserId,
|
|
534
|
+
reporterPrincipalSubject: input.creatorPrincipalSubject,
|
|
535
|
+
reporterPrincipalScope: input.creatorPrincipalScope,
|
|
536
|
+
reporterVendorId: creatorVendorId,
|
|
537
|
+
bundle,
|
|
538
|
+
artifacts,
|
|
539
|
+
kernelChecksum: kernelArtifact.checksum,
|
|
540
|
+
createdAt
|
|
541
|
+
});
|
|
542
|
+
const comment = {
|
|
543
|
+
id: randomUUID(),
|
|
544
|
+
issueId: duplicate.id,
|
|
545
|
+
author: "Legato Agora",
|
|
546
|
+
kind: "system",
|
|
547
|
+
body: `\uB3D9\uC77C signature report\uAC00 \uB2E4\uC2DC \uC811\uC218\uB418\uC5C8\uC2B5\uB2C8\uB2E4. Occurrence #${(duplicate.occurrenceCount ?? 1) + 1}.`,
|
|
548
|
+
createdAt
|
|
549
|
+
};
|
|
550
|
+
duplicate.currentBundleId = bundle.id;
|
|
551
|
+
duplicate.artifactIds = [...duplicate.artifactIds ?? [], ...artifacts.map((artifact) => artifact.id)];
|
|
552
|
+
duplicate.commentIds = [...duplicate.commentIds ?? [], comment.id];
|
|
553
|
+
duplicate.occurrenceIds = [...duplicate.occurrenceIds ?? [], occurrence2.id];
|
|
554
|
+
duplicate.occurrenceCount = (duplicate.occurrenceCount ?? Math.max(1, duplicate.occurrenceIds.length)) + 1;
|
|
555
|
+
duplicate.updatedAt = createdAt;
|
|
556
|
+
db.bundles.push(bundle);
|
|
557
|
+
db.artifacts.push(...artifacts);
|
|
558
|
+
db.comments.push(comment);
|
|
559
|
+
db.occurrences.push(occurrence2);
|
|
560
|
+
await writeDb(db);
|
|
561
|
+
return duplicate.id;
|
|
562
|
+
}
|
|
563
|
+
const issue = {
|
|
564
|
+
id: randomUUID(),
|
|
565
|
+
title: input.title.trim(),
|
|
566
|
+
description: input.description.trim(),
|
|
567
|
+
status: "open",
|
|
568
|
+
creator: input.creator.trim() || "Kernel Developer",
|
|
569
|
+
creatorUserId: input.creatorUserId,
|
|
570
|
+
creatorPrincipalSubject: input.creatorPrincipalSubject,
|
|
571
|
+
creatorPrincipalScope: input.creatorPrincipalScope,
|
|
572
|
+
creatorVendorId,
|
|
573
|
+
currentBundleId: bundle.id,
|
|
574
|
+
artifactIds: artifacts.map((artifact) => artifact.id),
|
|
575
|
+
commentIds: [],
|
|
576
|
+
verificationRunIds: [],
|
|
577
|
+
tags: deriveTags({
|
|
578
|
+
title: input.title,
|
|
579
|
+
description: input.description,
|
|
580
|
+
kernelFilename: kernelArtifact.filename,
|
|
581
|
+
hasLog: Boolean(errorLogArtifact),
|
|
582
|
+
source: input.source ?? "web",
|
|
583
|
+
tags: input.tags
|
|
584
|
+
}),
|
|
585
|
+
fingerprint,
|
|
586
|
+
fingerprints: [fingerprint],
|
|
587
|
+
occurrenceCount: 1,
|
|
588
|
+
occurrenceIds: [],
|
|
589
|
+
createdAt,
|
|
590
|
+
updatedAt: createdAt
|
|
591
|
+
};
|
|
592
|
+
const occurrence = makeOccurrence({
|
|
593
|
+
issueId: issue.id,
|
|
594
|
+
reporter: issue.creator,
|
|
595
|
+
reporterUserId: issue.creatorUserId,
|
|
596
|
+
reporterPrincipalSubject: issue.creatorPrincipalSubject,
|
|
597
|
+
reporterPrincipalScope: issue.creatorPrincipalScope,
|
|
598
|
+
reporterVendorId: issue.creatorVendorId,
|
|
599
|
+
bundle,
|
|
600
|
+
artifacts,
|
|
601
|
+
kernelChecksum: kernelArtifact.checksum,
|
|
602
|
+
createdAt
|
|
603
|
+
});
|
|
604
|
+
issue.occurrenceIds.push(occurrence.id);
|
|
605
|
+
const systemComment = {
|
|
606
|
+
id: randomUUID(),
|
|
607
|
+
issueId: issue.id,
|
|
608
|
+
author: "Legato Agora",
|
|
609
|
+
kind: "system",
|
|
610
|
+
body: bundle.source === "cli" ? "CLI report received with local reproduction metadata. Lowering and fsim automation will be attached in M2." : "Manual web report created. Prefer CLI reporting when reproducibility metadata matters.",
|
|
611
|
+
createdAt
|
|
612
|
+
};
|
|
613
|
+
issue.commentIds.push(systemComment.id);
|
|
614
|
+
db.issues.unshift(issue);
|
|
615
|
+
db.bundles.push(bundle);
|
|
616
|
+
db.artifacts.push(...artifacts);
|
|
617
|
+
db.comments.push(systemComment);
|
|
618
|
+
db.occurrences.push(occurrence);
|
|
619
|
+
await writeDb(db);
|
|
620
|
+
await notifyIssueCreatedBestEffort(issue, issue.creatorUserId);
|
|
621
|
+
return issue.id;
|
|
622
|
+
}
|
|
623
|
+
async function addComment(input) {
|
|
624
|
+
const db = await readDb();
|
|
625
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
626
|
+
const createdAt = now();
|
|
627
|
+
if (!issue) {
|
|
628
|
+
await addPortableComment(input, createdAt);
|
|
629
|
+
return;
|
|
630
|
+
}
|
|
631
|
+
const comment = {
|
|
632
|
+
id: randomUUID(),
|
|
633
|
+
issueId: input.issueId,
|
|
634
|
+
author: input.author.trim() || "Language Developer",
|
|
635
|
+
authorUserId: input.authorUserId,
|
|
636
|
+
authorPrincipalSubject: input.authorPrincipalSubject,
|
|
637
|
+
authorPrincipalScope: input.authorPrincipalScope,
|
|
638
|
+
kind: input.kind,
|
|
639
|
+
body: input.body.trim(),
|
|
640
|
+
createdAt
|
|
641
|
+
};
|
|
642
|
+
issue.commentIds.push(comment.id);
|
|
643
|
+
issue.updatedAt = createdAt;
|
|
644
|
+
if (comment.kind === "solution") {
|
|
645
|
+
issue.status = "resolved";
|
|
646
|
+
} else if (comment.kind === "analysis" && issue.status === "open") {
|
|
647
|
+
issue.status = "investigating";
|
|
648
|
+
}
|
|
649
|
+
db.comments.push(comment);
|
|
650
|
+
await writeDb(db);
|
|
651
|
+
await notifyIssueChangedBestEffort(issue, input.authorUserId, "comment_added");
|
|
652
|
+
}
|
|
653
|
+
async function addPortableComment(input, createdAt) {
|
|
654
|
+
assertSafeIssueId(input.issueId);
|
|
655
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
656
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
657
|
+
const portable = JSON.parse(raw);
|
|
658
|
+
const comment = {
|
|
659
|
+
id: randomUUID(),
|
|
660
|
+
issueId: portable.id,
|
|
661
|
+
author: input.author.trim() || "Language Developer",
|
|
662
|
+
authorUserId: input.authorUserId,
|
|
663
|
+
authorPrincipalSubject: input.authorPrincipalSubject,
|
|
664
|
+
authorPrincipalScope: input.authorPrincipalScope,
|
|
665
|
+
kind: input.kind,
|
|
666
|
+
body: input.body.trim(),
|
|
667
|
+
createdAt
|
|
668
|
+
};
|
|
669
|
+
portable.comments = [...portable.comments ?? [], comment];
|
|
670
|
+
portable.updatedAt = createdAt;
|
|
671
|
+
if (comment.kind === "solution") {
|
|
672
|
+
portable.status = "resolved";
|
|
673
|
+
} else if (comment.kind === "analysis" && (portable.status ?? "open") === "open") {
|
|
674
|
+
portable.status = "investigating";
|
|
675
|
+
}
|
|
676
|
+
await writePortableIssueJson(issuePath, portable);
|
|
677
|
+
}
|
|
678
|
+
async function updatePortableComment(input, updatedAt) {
|
|
679
|
+
assertSafeIssueId(input.issueId);
|
|
680
|
+
const issuePath = await readablePortableIssuePath(input.issueId);
|
|
681
|
+
if (!issuePath) return { ok: false, reason: "issue_not_found" };
|
|
682
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
683
|
+
const portable = JSON.parse(raw);
|
|
684
|
+
const comments = portable.comments ?? [];
|
|
685
|
+
const comment = comments.find((item) => item.id === input.commentId);
|
|
686
|
+
if (!comment) {
|
|
687
|
+
return input.commentId === `${portable.id}-system-comment` ? { ok: false, reason: "system_comment_protected" } : { ok: false, reason: "comment_not_found" };
|
|
688
|
+
}
|
|
689
|
+
if (comment.kind === "system") return { ok: false, reason: "system_comment_protected" };
|
|
690
|
+
if (!canMutateComment(comment, input.author, input.authorUserId, input.authorPrincipalSubject, input.authorPrincipalScope, input.authPolicy)) return { ok: false, reason: "comment_forbidden" };
|
|
691
|
+
const actor = input.author.trim();
|
|
692
|
+
const updatedComment = {
|
|
693
|
+
...comment,
|
|
694
|
+
kind: input.kind,
|
|
695
|
+
body: input.body.trim(),
|
|
696
|
+
editedAt: updatedAt,
|
|
697
|
+
editedBy: actor || "Language Developer",
|
|
698
|
+
editedByUserId: input.authorUserId,
|
|
699
|
+
editedByPrincipalSubject: input.authorPrincipalSubject,
|
|
700
|
+
editedByPrincipalScope: input.authorPrincipalScope
|
|
701
|
+
};
|
|
702
|
+
portable.comments = comments.map((item) => item.id === updatedComment.id ? updatedComment : item);
|
|
703
|
+
portable.updatedAt = updatedAt;
|
|
704
|
+
await writePortableIssueJson(issuePath, portable);
|
|
705
|
+
return hydratedPortableCommentMutationResult(input.issueId, input.commentId);
|
|
706
|
+
}
|
|
707
|
+
async function deletePortableComment(input, deletedAt) {
|
|
708
|
+
assertSafeIssueId(input.issueId);
|
|
709
|
+
const issuePath = await readablePortableIssuePath(input.issueId);
|
|
710
|
+
if (!issuePath) return { ok: false, reason: "issue_not_found" };
|
|
711
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
712
|
+
const portable = JSON.parse(raw);
|
|
713
|
+
const comments = portable.comments ?? [];
|
|
714
|
+
const comment = comments.find((item) => item.id === input.commentId);
|
|
715
|
+
if (!comment) {
|
|
716
|
+
return input.commentId === `${portable.id}-system-comment` ? { ok: false, reason: "system_comment_protected" } : { ok: false, reason: "comment_not_found" };
|
|
717
|
+
}
|
|
718
|
+
if (comment.kind === "system") return { ok: false, reason: "system_comment_protected" };
|
|
719
|
+
if (!canMutateComment(comment, input.actor, input.actorUserId, input.actorPrincipalSubject, input.actorPrincipalScope, input.authPolicy)) return { ok: false, reason: "comment_forbidden" };
|
|
720
|
+
const actor = input.actor.trim();
|
|
721
|
+
const deletedComment = {
|
|
722
|
+
...comment,
|
|
723
|
+
deletedAt,
|
|
724
|
+
deletedBy: actor || "Language Developer",
|
|
725
|
+
deletedByUserId: input.actorUserId,
|
|
726
|
+
deletedByPrincipalSubject: input.actorPrincipalSubject,
|
|
727
|
+
deletedByPrincipalScope: input.actorPrincipalScope
|
|
728
|
+
};
|
|
729
|
+
portable.comments = comments.map((item) => item.id === deletedComment.id ? deletedComment : item);
|
|
730
|
+
portable.updatedAt = deletedAt;
|
|
731
|
+
await writePortableIssueJson(issuePath, portable);
|
|
732
|
+
return hydratedPortableCommentMutationResult(input.issueId, input.commentId);
|
|
733
|
+
}
|
|
734
|
+
async function readablePortableIssuePath(issueId) {
|
|
735
|
+
try {
|
|
736
|
+
return await portableIssuePathFollowingDuplicate(issueId);
|
|
737
|
+
} catch (error) {
|
|
738
|
+
if (isFileNotFoundError(error)) return void 0;
|
|
739
|
+
throw error;
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
async function hydratedPortableCommentMutationResult(issueId, commentId) {
|
|
743
|
+
const record = await readPortableIssue(issueId, { followDuplicate: true });
|
|
744
|
+
if (!record) return { ok: false, reason: "issue_not_found" };
|
|
745
|
+
const comment = record.comments.find((item) => item.id === commentId);
|
|
746
|
+
return comment ? { ok: true, comment } : { ok: false, reason: "comment_not_found" };
|
|
747
|
+
}
|
|
748
|
+
function isFileNotFoundError(error) {
|
|
749
|
+
return error instanceof Error && hasNodeErrorCode(error) && error.code === "ENOENT";
|
|
750
|
+
}
|
|
751
|
+
function hasNodeErrorCode(error) {
|
|
752
|
+
return "code" in error && typeof error.code === "string";
|
|
753
|
+
}
|
|
754
|
+
function canMutateComment(comment, actor, actorUserId, actorPrincipalSubject, actorPrincipalScope, authPolicy) {
|
|
755
|
+
if (actorUserId && actorPrincipalSubject && actorPrincipalScope === "admin") return true;
|
|
756
|
+
if (comment.authorUserId && sameOptionalId(comment.authorUserId, actorUserId)) return true;
|
|
757
|
+
if (isSameTenantPrincipalLegacyOwner(comment.authorUserId, actorUserId, actorPrincipalSubject)) return true;
|
|
758
|
+
if (comment.authorUserId) return false;
|
|
759
|
+
if (authPolicy?.enforceTenantIdentity) return false;
|
|
760
|
+
return comment.author.trim().toLowerCase() === actor.trim().toLowerCase();
|
|
761
|
+
}
|
|
762
|
+
function isSameTenantPrincipalLegacyOwner(ownerUserId, actorUserId, actorPrincipalSubject) {
|
|
763
|
+
return Boolean(actorUserId && actorPrincipalSubject && ownerUserId === `tenant-principal:${actorPrincipalSubject}`);
|
|
764
|
+
}
|
|
765
|
+
async function updateComment(input) {
|
|
766
|
+
const db = await readDb();
|
|
767
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
768
|
+
const updatedAt = now();
|
|
769
|
+
if (!issue) return updatePortableComment(input, updatedAt);
|
|
770
|
+
const comment = db.comments.find((item) => item.id === input.commentId && item.issueId === input.issueId);
|
|
771
|
+
if (!comment || !issue.commentIds.includes(comment.id)) return { ok: false, reason: "comment_not_found" };
|
|
772
|
+
if (comment.kind === "system") return { ok: false, reason: "system_comment_protected" };
|
|
773
|
+
if (!canMutateComment(comment, input.author, input.authorUserId, input.authorPrincipalSubject, input.authorPrincipalScope, input.authPolicy)) return { ok: false, reason: "comment_forbidden" };
|
|
774
|
+
const updatedComment = {
|
|
775
|
+
...comment,
|
|
776
|
+
kind: input.kind,
|
|
777
|
+
body: input.body.trim(),
|
|
778
|
+
editedAt: updatedAt,
|
|
779
|
+
editedBy: input.author.trim() || "Language Developer",
|
|
780
|
+
editedByUserId: input.authorUserId,
|
|
781
|
+
editedByPrincipalSubject: input.authorPrincipalSubject,
|
|
782
|
+
editedByPrincipalScope: input.authorPrincipalScope
|
|
783
|
+
};
|
|
784
|
+
db.comments = db.comments.map((item) => item.id === updatedComment.id ? updatedComment : item);
|
|
785
|
+
issue.updatedAt = updatedAt;
|
|
786
|
+
await writeDb(db);
|
|
787
|
+
await notifyIssueChangedBestEffort(issue, input.authorUserId, "comment_updated");
|
|
788
|
+
return { ok: true, comment: updatedComment };
|
|
789
|
+
}
|
|
790
|
+
async function deleteComment(input) {
|
|
791
|
+
const db = await readDb();
|
|
792
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
793
|
+
const deletedAt = now();
|
|
794
|
+
if (!issue) return deletePortableComment(input, deletedAt);
|
|
795
|
+
const comment = db.comments.find((item) => item.id === input.commentId && item.issueId === input.issueId);
|
|
796
|
+
if (!comment || !issue.commentIds.includes(comment.id)) return { ok: false, reason: "comment_not_found" };
|
|
797
|
+
if (comment.kind === "system") return { ok: false, reason: "system_comment_protected" };
|
|
798
|
+
if (!canMutateComment(comment, input.actor, input.actorUserId, input.actorPrincipalSubject, input.actorPrincipalScope, input.authPolicy)) return { ok: false, reason: "comment_forbidden" };
|
|
799
|
+
const deletedComment = {
|
|
800
|
+
...comment,
|
|
801
|
+
deletedAt,
|
|
802
|
+
deletedBy: input.actor.trim() || "Language Developer",
|
|
803
|
+
deletedByUserId: input.actorUserId,
|
|
804
|
+
deletedByPrincipalSubject: input.actorPrincipalSubject,
|
|
805
|
+
deletedByPrincipalScope: input.actorPrincipalScope
|
|
806
|
+
};
|
|
807
|
+
db.comments = db.comments.map((item) => item.id === deletedComment.id ? deletedComment : item);
|
|
808
|
+
issue.updatedAt = deletedAt;
|
|
809
|
+
await writeDb(db);
|
|
810
|
+
await notifyIssueChangedBestEffort(issue, input.actorUserId, "comment_deleted");
|
|
811
|
+
return { ok: true, comment: deletedComment };
|
|
812
|
+
}
|
|
813
|
+
async function closeIssue(issueId, actor = "Language Developer", actorUserId, actorPrincipalSubject, actorPrincipalScope) {
|
|
814
|
+
await updateIssueStatus({ issueId, status: "resolved", actor, actorUserId, actorPrincipalSubject, actorPrincipalScope });
|
|
815
|
+
}
|
|
816
|
+
async function deleteIssue(issueId, actorUserId) {
|
|
817
|
+
assertSafeIssueId(issueId);
|
|
818
|
+
const db = await readDb();
|
|
819
|
+
const issue = db.issues.find((item) => item.id === issueId);
|
|
820
|
+
if (!issue) {
|
|
821
|
+
const record = await getIssue(issueId);
|
|
822
|
+
const deleted = await deletePortableIssue(issueId);
|
|
823
|
+
if (!hasIsolatedIssueStore && deleted && record) await notifyIssueDeletedBestEffort(record.issue, actorUserId);
|
|
824
|
+
return deleted;
|
|
825
|
+
}
|
|
826
|
+
const artifactIds = new Set(issue.artifactIds ?? []);
|
|
827
|
+
const commentIds = new Set(issue.commentIds ?? []);
|
|
828
|
+
const verificationRunIds = new Set(issue.verificationRunIds ?? []);
|
|
829
|
+
const occurrenceIds = new Set(issue.occurrenceIds ?? []);
|
|
830
|
+
const bundleIds = /* @__PURE__ */ new Set([
|
|
831
|
+
issue.currentBundleId,
|
|
832
|
+
...db.occurrences.filter((occurrence) => occurrence.issueId === issueId || occurrenceIds.has(occurrence.id)).map((occurrence) => occurrence.bundleId)
|
|
833
|
+
]);
|
|
834
|
+
const localArtifacts = db.artifacts.filter((artifact) => artifactIds.has(artifact.id) && artifact.storage !== "issue-store");
|
|
835
|
+
await writeDb({
|
|
836
|
+
issues: db.issues.filter((item) => item.id !== issueId),
|
|
837
|
+
bundles: db.bundles.filter((bundle) => !bundleIds.has(bundle.id)),
|
|
838
|
+
artifacts: db.artifacts.filter((artifact) => !artifactIds.has(artifact.id)),
|
|
839
|
+
comments: db.comments.filter((comment) => comment.issueId !== issueId && !commentIds.has(comment.id)),
|
|
840
|
+
verificationRuns: db.verificationRuns.filter((run) => run.issueId !== issueId && !verificationRunIds.has(run.id)),
|
|
841
|
+
occurrences: db.occurrences.filter((occurrence) => occurrence.issueId !== issueId && !occurrenceIds.has(occurrence.id))
|
|
842
|
+
});
|
|
843
|
+
await Promise.all(localArtifacts.map((artifact) => rm(safeArtifactPath(dataRoot, artifact.relativePath), { force: true })));
|
|
844
|
+
await notifyIssueDeletedBestEffort(issue, actorUserId);
|
|
845
|
+
return true;
|
|
846
|
+
}
|
|
847
|
+
async function updateIssueStatus(issueIdOrInput, status, actor = "Language Developer", actorUserId) {
|
|
848
|
+
const input = typeof issueIdOrInput === "string" ? { issueId: issueIdOrInput, status: status ?? "open", actor, actorUserId } : issueIdOrInput;
|
|
849
|
+
assertSafeIssueId(input.issueId);
|
|
850
|
+
const db = await readDb();
|
|
851
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
852
|
+
const updatedAt = now();
|
|
853
|
+
if (issue) {
|
|
854
|
+
const previousStatus = issue.status;
|
|
855
|
+
issue.status = input.status;
|
|
856
|
+
issue.updatedAt = updatedAt;
|
|
857
|
+
const comment = statusChangeComment(input, previousStatus, updatedAt);
|
|
858
|
+
issue.commentIds.push(comment.id);
|
|
859
|
+
db.comments.push(comment);
|
|
860
|
+
await writeDb(db);
|
|
861
|
+
await notifyIssueChangedBestEffort(issue, input.actorUserId, "status_changed");
|
|
862
|
+
return;
|
|
863
|
+
}
|
|
864
|
+
await updatePortableIssueStatus(input, updatedAt);
|
|
865
|
+
}
|
|
866
|
+
async function updateIssueDescription(issueIdOrInput, description, actor = "Language Developer", actorUserId) {
|
|
867
|
+
const input = typeof issueIdOrInput === "string" ? { issueId: issueIdOrInput, description: description ?? "", actor, actorUserId } : issueIdOrInput;
|
|
868
|
+
assertSafeIssueId(input.issueId);
|
|
869
|
+
const updatedAt = now();
|
|
870
|
+
const db = await readDb();
|
|
871
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
872
|
+
if (issue) {
|
|
873
|
+
if (!canMutateIssueDescription(issue.creator, issue.creatorUserId, input.actor, input.actorUserId, input.actorPrincipalSubject, input.actorPrincipalScope, input.authPolicy)) {
|
|
874
|
+
throw new Error("issue_description_forbidden");
|
|
875
|
+
}
|
|
876
|
+
issue.description = input.description.trim();
|
|
877
|
+
issue.updatedAt = updatedAt;
|
|
878
|
+
await writeDb(db);
|
|
879
|
+
await notifyIssueChangedBestEffort(issue, input.actorUserId, "issue_description_updated");
|
|
880
|
+
return issue;
|
|
881
|
+
}
|
|
882
|
+
await updatePortableIssueDescription(input, updatedAt);
|
|
883
|
+
const record = await getIssue(input.issueId);
|
|
884
|
+
if (!hasIsolatedIssueStore && record) await notifyIssueChangedBestEffort(record.issue, input.actorUserId, "issue_description_updated");
|
|
885
|
+
if (!record) throw new Error("issue_not_found");
|
|
886
|
+
return record.issue;
|
|
887
|
+
}
|
|
888
|
+
async function addIssueTags(issueId, tags) {
|
|
889
|
+
const registry = await listTagDefinitions();
|
|
890
|
+
const allowed = new Set(registry.map((tag) => tag.id));
|
|
891
|
+
const normalizedTags = uniqueTags2(tags).filter((tag) => allowed.has(tag));
|
|
892
|
+
if (normalizedTags.length === 0) return;
|
|
893
|
+
const db = await readDb();
|
|
894
|
+
const issue = db.issues.find((item) => item.id === issueId);
|
|
895
|
+
const updatedAt = now();
|
|
896
|
+
if (issue) {
|
|
897
|
+
issue.tags = uniqueTags2([...issue.tags ?? [], ...normalizedTags]);
|
|
898
|
+
issue.updatedAt = updatedAt;
|
|
899
|
+
await writeDb(db);
|
|
900
|
+
return;
|
|
901
|
+
}
|
|
902
|
+
await addPortableIssueTags(issueId, normalizedTags, updatedAt);
|
|
903
|
+
}
|
|
904
|
+
async function assignIssue(input) {
|
|
905
|
+
assertSafeIssueId(input.issueId);
|
|
906
|
+
const assignee = new IssueAssigneeName(input.assignee).value;
|
|
907
|
+
const updatedAt = now();
|
|
908
|
+
const db = await readDb();
|
|
909
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
910
|
+
if (issue) {
|
|
911
|
+
const nextAssignee = IssueAssignment.create({
|
|
912
|
+
assignee,
|
|
913
|
+
assigneeUserId: input.assigneeUserId,
|
|
914
|
+
actor: input.actor,
|
|
915
|
+
actorUserId: input.actorUserId,
|
|
916
|
+
actorPrincipalSubject: input.actorPrincipalSubject,
|
|
917
|
+
actorPrincipalScope: input.actorPrincipalScope,
|
|
918
|
+
assignedAt: updatedAt
|
|
919
|
+
});
|
|
920
|
+
const assignees = [
|
|
921
|
+
...issueAssignees(issue).filter((item) => item.name.toLowerCase() !== nextAssignee.name.toLowerCase()),
|
|
922
|
+
nextAssignee
|
|
923
|
+
];
|
|
924
|
+
issue.assignees = assignees;
|
|
925
|
+
issue.assignee = firstAssignee(assignees);
|
|
926
|
+
issue.updatedAt = updatedAt;
|
|
927
|
+
const comment = assignmentComment(input.issueId, assignee, input.actor, updatedAt);
|
|
928
|
+
db.comments.push(comment);
|
|
929
|
+
issue.commentIds.push(comment.id);
|
|
930
|
+
await writeDb(db);
|
|
931
|
+
await notifyIssueChangedBestEffort(issue, input.actorUserId, "assignment_changed");
|
|
932
|
+
return;
|
|
933
|
+
}
|
|
934
|
+
await assignPortableIssue(input, updatedAt);
|
|
935
|
+
}
|
|
936
|
+
async function unassignIssue(input) {
|
|
937
|
+
assertSafeIssueId(input.issueId);
|
|
938
|
+
const assignee = new IssueAssigneeName(input.assignee).value;
|
|
939
|
+
const updatedAt = now();
|
|
940
|
+
const db = await readDb();
|
|
941
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
942
|
+
if (issue) {
|
|
943
|
+
const assignees = issueAssignees(issue).filter((item) => item.name.toLowerCase() !== assignee.toLowerCase());
|
|
944
|
+
issue.assignees = assignees;
|
|
945
|
+
issue.assignee = firstAssignee(assignees);
|
|
946
|
+
issue.updatedAt = updatedAt;
|
|
947
|
+
const comment = unassignmentComment(input.issueId, input.actor, updatedAt, assignee);
|
|
948
|
+
db.comments.push(comment);
|
|
949
|
+
issue.commentIds.push(comment.id);
|
|
950
|
+
await writeDb(db);
|
|
951
|
+
await notifyIssueChangedBestEffort(issue, input.actorUserId, "assignment_changed");
|
|
952
|
+
return;
|
|
953
|
+
}
|
|
954
|
+
await unassignPortableIssue(input, updatedAt);
|
|
955
|
+
}
|
|
956
|
+
async function clearIssueAssignee(issueId, actor = "Language Developer") {
|
|
957
|
+
assertSafeIssueId(issueId);
|
|
958
|
+
const updatedAt = now();
|
|
959
|
+
const db = await readDb();
|
|
960
|
+
const issue = db.issues.find((item) => item.id === issueId);
|
|
961
|
+
if (issue) {
|
|
962
|
+
issue.assignees = [];
|
|
963
|
+
issue.assignee = void 0;
|
|
964
|
+
issue.updatedAt = updatedAt;
|
|
965
|
+
const comment = unassignmentComment(issueId, actor, updatedAt);
|
|
966
|
+
db.comments.push(comment);
|
|
967
|
+
issue.commentIds.push(comment.id);
|
|
968
|
+
await writeDb(db);
|
|
969
|
+
await notifyIssueChangedBestEffort(issue, void 0, "assignment_changed");
|
|
970
|
+
return;
|
|
971
|
+
}
|
|
972
|
+
await clearPortableIssueAssignee(issueId, actor, updatedAt);
|
|
973
|
+
}
|
|
974
|
+
async function addIssueBlocker(input) {
|
|
975
|
+
assertSafeIssueId(input.issueId);
|
|
976
|
+
if (!Number.isInteger(input.number) || input.number <= 0) {
|
|
977
|
+
throw new Error(`Unsupported pull request number: ${input.number}`);
|
|
978
|
+
}
|
|
979
|
+
const repository = new GithubRepository(input.repository).value;
|
|
980
|
+
const createdAt = now();
|
|
981
|
+
const blocker = {
|
|
982
|
+
id: randomUUID(),
|
|
983
|
+
kind: "github-pr",
|
|
984
|
+
repository,
|
|
985
|
+
number: input.number,
|
|
986
|
+
url: githubPullRequestUrl(repository, input.number),
|
|
987
|
+
status: "unknown",
|
|
988
|
+
addedBy: input.actor.trim() || "Language Developer",
|
|
989
|
+
addedByUserId: input.actorUserId,
|
|
990
|
+
addedByPrincipalSubject: input.actorPrincipalSubject,
|
|
991
|
+
addedByPrincipalScope: input.actorPrincipalScope,
|
|
992
|
+
addedAt: createdAt
|
|
993
|
+
};
|
|
994
|
+
const synced = await new GithubPullRequestSyncService().sync(blocker);
|
|
995
|
+
const db = await readDb();
|
|
996
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
997
|
+
if (issue) {
|
|
998
|
+
const blockers = [...(issue.blockers ?? []).filter((item) => item.repository !== synced.repository || item.number !== synced.number), synced];
|
|
999
|
+
issue.blockers = blockers;
|
|
1000
|
+
issue.tags = IssueBlockerTags.merge(issue.tags ?? [], blockers);
|
|
1001
|
+
issue.updatedAt = synced.lastSyncedAt ?? createdAt;
|
|
1002
|
+
const comment = blockerComment(input.issueId, synced, input.actor, createdAt);
|
|
1003
|
+
db.comments.push(comment);
|
|
1004
|
+
issue.commentIds.push(comment.id);
|
|
1005
|
+
await writeDb(db);
|
|
1006
|
+
await notifyIssueChangedBestEffort(issue, input.actorUserId, "blocker_added");
|
|
1007
|
+
return synced;
|
|
1008
|
+
}
|
|
1009
|
+
await addPortableIssueBlocker(input.issueId, synced, input.actor, createdAt);
|
|
1010
|
+
return synced;
|
|
1011
|
+
}
|
|
1012
|
+
async function syncIssueBlocker(input) {
|
|
1013
|
+
assertSafeIssueId(input.issueId);
|
|
1014
|
+
const db = await readDb();
|
|
1015
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
1016
|
+
if (issue) {
|
|
1017
|
+
const blocker = (issue.blockers ?? []).find((item) => item.id === input.blockerId);
|
|
1018
|
+
if (!blocker) throw new Error(`Blocker not found: ${input.blockerId}`);
|
|
1019
|
+
const synced = await new GithubPullRequestSyncService().sync(blocker);
|
|
1020
|
+
issue.blockers = (issue.blockers ?? []).map((item) => item.id === synced.id ? synced : item);
|
|
1021
|
+
issue.tags = IssueBlockerTags.merge(issue.tags ?? [], issue.blockers);
|
|
1022
|
+
issue.updatedAt = synced.lastSyncedAt ?? now();
|
|
1023
|
+
const comment = blockerSyncComment(input.issueId, synced, input.actor, issue.updatedAt);
|
|
1024
|
+
db.comments.push(comment);
|
|
1025
|
+
issue.commentIds.push(comment.id);
|
|
1026
|
+
await writeDb(db);
|
|
1027
|
+
await notifyIssueChangedBestEffort(issue, input.actorUserId, "blocker_synced");
|
|
1028
|
+
return synced;
|
|
1029
|
+
}
|
|
1030
|
+
return syncPortableIssueBlocker(input);
|
|
1031
|
+
}
|
|
1032
|
+
async function removeIssueBlocker(input) {
|
|
1033
|
+
assertSafeIssueId(input.issueId);
|
|
1034
|
+
const updatedAt = now();
|
|
1035
|
+
const db = await readDb();
|
|
1036
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
1037
|
+
if (issue) {
|
|
1038
|
+
const blocker = (issue.blockers ?? []).find((item) => item.id === input.blockerId);
|
|
1039
|
+
if (!blocker) throw new Error(`Blocker not found: ${input.blockerId}`);
|
|
1040
|
+
issue.blockers = (issue.blockers ?? []).filter((item) => item.id !== input.blockerId);
|
|
1041
|
+
issue.tags = IssueBlockerTags.merge(issue.tags ?? [], issue.blockers);
|
|
1042
|
+
issue.updatedAt = updatedAt;
|
|
1043
|
+
const comment = blockerRemovedComment(input.issueId, blocker, input.actor, updatedAt);
|
|
1044
|
+
db.comments.push(comment);
|
|
1045
|
+
issue.commentIds.push(comment.id);
|
|
1046
|
+
await writeDb(db);
|
|
1047
|
+
await notifyIssueChangedBestEffort(issue, input.actorUserId, "blocker_removed");
|
|
1048
|
+
return;
|
|
1049
|
+
}
|
|
1050
|
+
await removePortableIssueBlocker(input, updatedAt);
|
|
1051
|
+
}
|
|
1052
|
+
async function listIssues() {
|
|
1053
|
+
const db = await readDb();
|
|
1054
|
+
const localIssues = db.issues.map((issue) => hydrateIssue(db, issue)).filter(Boolean);
|
|
1055
|
+
const portableIssues = await readPortableIssues();
|
|
1056
|
+
return [...portableIssues, ...localIssues].sort((left, right) => {
|
|
1057
|
+
const leftDone = left.issue.status === "resolved" || left.issue.status === "deferred" || left.issue.status === "cancelled";
|
|
1058
|
+
const rightDone = right.issue.status === "resolved" || right.issue.status === "deferred" || right.issue.status === "cancelled";
|
|
1059
|
+
if (leftDone !== rightDone) return leftDone ? 1 : -1;
|
|
1060
|
+
return new Date(right.issue.updatedAt).getTime() - new Date(left.issue.updatedAt).getTime();
|
|
1061
|
+
});
|
|
1062
|
+
}
|
|
1063
|
+
async function getIssue(id) {
|
|
1064
|
+
assertSafeIssueId(id);
|
|
1065
|
+
const db = await readDb();
|
|
1066
|
+
const issue = db.issues.find((item) => item.id === id);
|
|
1067
|
+
if (issue) return hydrateIssue(db, issue);
|
|
1068
|
+
return readPortableIssue(id, { followDuplicate: true });
|
|
1069
|
+
}
|
|
1070
|
+
async function readArtifactText(artifact) {
|
|
1071
|
+
return readFile2(getArtifactTextPath(artifact), "utf8");
|
|
1072
|
+
}
|
|
1073
|
+
function getArtifactTextPath(artifact) {
|
|
1074
|
+
return hasIsolatedIssueStore || artifact.storage === "issue-store" ? existingIssueStoreArtifactPath(artifact.relativePath) : safeArtifactPath(dataRoot, artifact.relativePath);
|
|
1075
|
+
}
|
|
1076
|
+
async function addKernelSuggestion(input) {
|
|
1077
|
+
assertSafeIssueId(input.issueId);
|
|
1078
|
+
assertSafeFilename(input.filename);
|
|
1079
|
+
const db = await readDb();
|
|
1080
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
1081
|
+
if (issue) {
|
|
1082
|
+
const artifact = await writeLocalKernelSuggestionArtifact(input, now());
|
|
1083
|
+
const comment = kernelSuggestionComment(input, artifact.createdAt);
|
|
1084
|
+
issue.artifactIds = [...issue.artifactIds ?? [], artifact.id];
|
|
1085
|
+
issue.commentIds = [...issue.commentIds ?? [], comment.id];
|
|
1086
|
+
issue.updatedAt = artifact.createdAt;
|
|
1087
|
+
db.artifacts.push(artifact);
|
|
1088
|
+
db.comments.push(comment);
|
|
1089
|
+
await writeDb(db);
|
|
1090
|
+
await notifyIssueChangedBestEffort(issue, input.authorUserId, "suggestion_added");
|
|
1091
|
+
return artifact;
|
|
1092
|
+
}
|
|
1093
|
+
return addPortableKernelSuggestion(input);
|
|
1094
|
+
}
|
|
1095
|
+
async function updateKernelSuggestion(input) {
|
|
1096
|
+
assertSafeIssueId(input.issueId);
|
|
1097
|
+
assertSafeFilename(input.filename);
|
|
1098
|
+
const db = await readDb();
|
|
1099
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
1100
|
+
if (issue) {
|
|
1101
|
+
const artifactIndex = db.artifacts.findIndex((artifact2) => artifact2.id === input.suggestionArtifactId);
|
|
1102
|
+
const artifact = artifactIndex >= 0 ? db.artifacts[artifactIndex] : void 0;
|
|
1103
|
+
if (!artifact || artifact.kind !== "kernel-suggestion" || !(issue.artifactIds ?? []).includes(artifact.id)) {
|
|
1104
|
+
throw new Error(`Kernel suggestion not found: ${input.suggestionArtifactId}`);
|
|
1105
|
+
}
|
|
1106
|
+
const updatedAt = now();
|
|
1107
|
+
const updatedArtifact = await rewriteKernelSuggestionArtifact(artifact, input.content);
|
|
1108
|
+
const comment = kernelSuggestionUpdatedComment(input, updatedAt);
|
|
1109
|
+
db.artifacts[artifactIndex] = updatedArtifact;
|
|
1110
|
+
db.comments.push(comment);
|
|
1111
|
+
issue.commentIds = [...issue.commentIds ?? [], comment.id];
|
|
1112
|
+
issue.updatedAt = updatedAt;
|
|
1113
|
+
await writeDb(db);
|
|
1114
|
+
await notifyIssueChangedBestEffort(issue, input.authorUserId, "suggestion_updated");
|
|
1115
|
+
return updatedArtifact;
|
|
1116
|
+
}
|
|
1117
|
+
return updatePortableKernelSuggestion(input);
|
|
1118
|
+
}
|
|
1119
|
+
async function recordVerificationResult(input) {
|
|
1120
|
+
assertSafeIssueId(input.issueId);
|
|
1121
|
+
const db = await readDb();
|
|
1122
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
1123
|
+
if (issue) {
|
|
1124
|
+
const run2 = await recordLocalVerificationResult(db, issue, input);
|
|
1125
|
+
await writeDb(db);
|
|
1126
|
+
await notifyVerificationChangedBestEffort(issue, run2, input.actorUserId);
|
|
1127
|
+
return run2;
|
|
1128
|
+
}
|
|
1129
|
+
const run = await recordPortableVerificationResult(input);
|
|
1130
|
+
const record = await getIssue(input.issueId);
|
|
1131
|
+
if (!hasIsolatedIssueStore && record) await notifyVerificationChangedBestEffort(record.issue, run, input.actorUserId);
|
|
1132
|
+
return run;
|
|
1133
|
+
}
|
|
1134
|
+
async function updateVerificationRunResult(input) {
|
|
1135
|
+
assertSafeIssueId(input.issueId);
|
|
1136
|
+
const db = await readDb();
|
|
1137
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
1138
|
+
if (issue) {
|
|
1139
|
+
const run2 = await updateLocalVerificationRun(db, issue, input);
|
|
1140
|
+
await writeDb(db);
|
|
1141
|
+
await notifyVerificationChangedBestEffort(issue, run2, input.actorUserId);
|
|
1142
|
+
return run2;
|
|
1143
|
+
}
|
|
1144
|
+
const run = await updatePortableVerificationRun(input);
|
|
1145
|
+
const record = await getIssue(input.issueId);
|
|
1146
|
+
if (!hasIsolatedIssueStore && record) await notifyVerificationChangedBestEffort(record.issue, run, input.actorUserId);
|
|
1147
|
+
return run;
|
|
1148
|
+
}
|
|
1149
|
+
async function deleteVerificationRuns(input) {
|
|
1150
|
+
assertSafeIssueId(input.issueId);
|
|
1151
|
+
const runIds = [...new Set(input.runIds.filter(Boolean))];
|
|
1152
|
+
if (runIds.length === 0) return { deletedRunIds: [] };
|
|
1153
|
+
const db = await readDb();
|
|
1154
|
+
const issue = db.issues.find((item) => item.id === input.issueId);
|
|
1155
|
+
if (!issue) {
|
|
1156
|
+
const record = await getIssue(input.issueId);
|
|
1157
|
+
const result = await deletePortableVerificationRuns(input.issueId, runIds);
|
|
1158
|
+
if (!hasIsolatedIssueStore && result.deletedRunIds.length > 0 && record) {
|
|
1159
|
+
await notifyVerificationDeletedBestEffort(record.issue, input.actorUserId);
|
|
1160
|
+
}
|
|
1161
|
+
return result;
|
|
1162
|
+
}
|
|
1163
|
+
const selectedRunIds = new Set(runIds);
|
|
1164
|
+
const runs = db.verificationRuns.filter((run) => run.issueId === input.issueId && selectedRunIds.has(run.id));
|
|
1165
|
+
const deletedRunIds = runs.map((run) => run.id);
|
|
1166
|
+
if (deletedRunIds.length === 0) return { deletedRunIds: [] };
|
|
1167
|
+
const artifactIds = new Set(runs.flatMap((run) => verificationRunArtifactIds(db.artifacts, run)));
|
|
1168
|
+
const localArtifacts = db.artifacts.filter((artifact) => artifactIds.has(artifact.id) && artifact.storage !== "issue-store");
|
|
1169
|
+
issue.verificationRunIds = (issue.verificationRunIds ?? []).filter((id) => !selectedRunIds.has(id));
|
|
1170
|
+
issue.artifactIds = (issue.artifactIds ?? []).filter((id) => !artifactIds.has(id));
|
|
1171
|
+
issue.updatedAt = now();
|
|
1172
|
+
db.verificationRuns = db.verificationRuns.filter((run) => !(run.issueId === input.issueId && selectedRunIds.has(run.id)));
|
|
1173
|
+
db.artifacts = db.artifacts.filter((artifact) => !artifactIds.has(artifact.id));
|
|
1174
|
+
await writeDb(db);
|
|
1175
|
+
await Promise.all(localArtifacts.map((artifact) => rm(safeArtifactPath(dataRoot, artifact.relativePath), { force: true })));
|
|
1176
|
+
await notifyVerificationDeletedBestEffort(issue, input.actorUserId);
|
|
1177
|
+
return { deletedRunIds };
|
|
1178
|
+
}
|
|
1179
|
+
async function hasRunningVerificationRun(input) {
|
|
1180
|
+
const record = await getIssue(input.issueId);
|
|
1181
|
+
if (!record) return false;
|
|
1182
|
+
const staleMs = verificationRunStaleMs();
|
|
1183
|
+
const nowMs = Date.now();
|
|
1184
|
+
return record.verificationRuns.some((run) => run.kind === input.kind && sameOptionalId(run.sourceArtifactId, input.sourceArtifactId) && isVerificationRunActive(run, staleMs, nowMs));
|
|
1185
|
+
}
|
|
1186
|
+
function hydrateIssue(db, issue) {
|
|
1187
|
+
const bundle = db.bundles.find((item) => item.id === issue.currentBundleId);
|
|
1188
|
+
if (!bundle) return null;
|
|
1189
|
+
const occurrences = (issue.occurrenceIds ?? []).map((id) => db.occurrences.find((occurrence) => occurrence.id === id)).filter(Boolean);
|
|
1190
|
+
const creatorVendorId = normalizeVendorId(issue.creatorVendorId);
|
|
1191
|
+
const hydratedOccurrences = occurrences.map((occurrence) => hydrateOccurrence(occurrence, creatorVendorId));
|
|
1192
|
+
const hydratedIssue = {
|
|
1193
|
+
...issue,
|
|
1194
|
+
creatorVendorId,
|
|
1195
|
+
assignee: firstAssignee(issueAssignees(issue)),
|
|
1196
|
+
assignees: issueAssignees(issue),
|
|
1197
|
+
tags: issue.tags ?? [],
|
|
1198
|
+
fingerprints: issue.fingerprints ?? (issue.fingerprint ? [issue.fingerprint] : []),
|
|
1199
|
+
occurrenceCount: issue.occurrenceCount ?? Math.max(1, occurrences.length),
|
|
1200
|
+
occurrenceIds: issue.occurrenceIds ?? hydratedOccurrences.map((occurrence) => occurrence.id)
|
|
1201
|
+
};
|
|
1202
|
+
return {
|
|
1203
|
+
issue: hydratedIssue,
|
|
1204
|
+
bundle,
|
|
1205
|
+
artifacts: issue.artifactIds.map((id) => db.artifacts.find((artifact) => artifact.id === id)).filter(Boolean),
|
|
1206
|
+
comments: issue.commentIds.map((id) => db.comments.find((comment) => comment.id === id)).filter(Boolean),
|
|
1207
|
+
verificationRuns: (issue.verificationRunIds ?? []).map((id) => db.verificationRuns.find((run) => run.id === id)).filter(Boolean),
|
|
1208
|
+
occurrences: hydratedOccurrences
|
|
1209
|
+
};
|
|
1210
|
+
}
|
|
1211
|
+
function hydrateOccurrence(occurrence, fallbackVendorId) {
|
|
1212
|
+
return {
|
|
1213
|
+
...occurrence,
|
|
1214
|
+
reporterVendorId: normalizeVendorId(occurrence.reporterVendorId ?? fallbackVendorId)
|
|
1215
|
+
};
|
|
1216
|
+
}
|
|
1217
|
+
async function recordLocalVerificationResult(db, issue, input) {
|
|
1218
|
+
const completedAt = input.completedAt ?? now();
|
|
1219
|
+
const startedAt = input.startedAt ?? completedAt;
|
|
1220
|
+
const artifact = await writeLocalVerificationArtifact(issue.id, input.kind, input.logText, completedAt);
|
|
1221
|
+
const generatedArtifacts = await writeLocalVerificationGeneratedArtifacts(input.generatedArtifactFiles, completedAt);
|
|
1222
|
+
const run = {
|
|
1223
|
+
id: randomUUID(),
|
|
1224
|
+
issueId: issue.id,
|
|
1225
|
+
bundleId: issue.currentBundleId,
|
|
1226
|
+
kind: input.kind,
|
|
1227
|
+
sourceArtifactId: input.sourceArtifactId,
|
|
1228
|
+
status: input.status,
|
|
1229
|
+
exitCode: input.exitCode,
|
|
1230
|
+
logArtifactId: artifact.id,
|
|
1231
|
+
toolVersion: input.toolVersion,
|
|
1232
|
+
failedStage: input.failedStage,
|
|
1233
|
+
completedStage: input.completedStage,
|
|
1234
|
+
generatedArtifacts: input.generatedArtifacts,
|
|
1235
|
+
generatedArtifactIds: generatedArtifacts.map((item) => item.id),
|
|
1236
|
+
sdkRepoUrl: input.sdkRepoUrl,
|
|
1237
|
+
sdkBranch: input.sdkBranch,
|
|
1238
|
+
sdkCommitSha: input.sdkCommitSha,
|
|
1239
|
+
sdkPreparedRoot: input.sdkPreparedRoot,
|
|
1240
|
+
fingerprint: input.fingerprint,
|
|
1241
|
+
startedAt,
|
|
1242
|
+
completedAt,
|
|
1243
|
+
requesterUserId: input.actorUserId,
|
|
1244
|
+
requesterPrincipalSubject: input.actorPrincipalSubject,
|
|
1245
|
+
requesterPrincipalScope: input.actorPrincipalScope
|
|
1246
|
+
};
|
|
1247
|
+
const comment = {
|
|
1248
|
+
id: randomUUID(),
|
|
1249
|
+
issueId: issue.id,
|
|
1250
|
+
author: "Legato Worker",
|
|
1251
|
+
kind: "system",
|
|
1252
|
+
body: input.commentBody ?? verificationComment(run, artifact.filename),
|
|
1253
|
+
createdAt: completedAt
|
|
1254
|
+
};
|
|
1255
|
+
issue.artifactIds = [...issue.artifactIds ?? [], artifact.id, ...generatedArtifacts.map((item) => item.id)];
|
|
1256
|
+
issue.commentIds = [...issue.commentIds ?? [], comment.id];
|
|
1257
|
+
issue.verificationRunIds = [...issue.verificationRunIds ?? [], run.id];
|
|
1258
|
+
if (input.fingerprint) {
|
|
1259
|
+
issue.fingerprint = input.fingerprint;
|
|
1260
|
+
issue.fingerprints = upsertFingerprint(issue.fingerprints ?? [], input.fingerprint);
|
|
1261
|
+
}
|
|
1262
|
+
issue.updatedAt = completedAt;
|
|
1263
|
+
db.artifacts.push(artifact, ...generatedArtifacts);
|
|
1264
|
+
db.comments.push(comment);
|
|
1265
|
+
db.verificationRuns.push(run);
|
|
1266
|
+
return run;
|
|
1267
|
+
}
|
|
1268
|
+
async function updateLocalVerificationRun(db, issue, input) {
|
|
1269
|
+
const completedAt = input.completedAt ?? now();
|
|
1270
|
+
const run = db.verificationRuns.find((item) => item.id === input.runId && item.issueId === issue.id);
|
|
1271
|
+
if (!run) throw new Error(`Verification run not found: ${input.runId}`);
|
|
1272
|
+
const artifact = run.logArtifactId ? db.artifacts.find((item) => item.id === run.logArtifactId) : void 0;
|
|
1273
|
+
if (!artifact) throw new Error(`Verification log artifact not found for run: ${input.runId}`);
|
|
1274
|
+
await rewriteVerificationArtifact(artifact, input.logText);
|
|
1275
|
+
const generatedArtifacts = await writeLocalVerificationGeneratedArtifacts(input.generatedArtifactFiles, completedAt);
|
|
1276
|
+
run.status = input.status;
|
|
1277
|
+
run.sourceArtifactId = input.sourceArtifactId ?? run.sourceArtifactId;
|
|
1278
|
+
run.exitCode = input.exitCode;
|
|
1279
|
+
run.toolVersion = input.toolVersion;
|
|
1280
|
+
run.failedStage = input.failedStage;
|
|
1281
|
+
run.completedStage = input.completedStage;
|
|
1282
|
+
run.generatedArtifacts = input.generatedArtifacts;
|
|
1283
|
+
if (generatedArtifacts.length > 0) run.generatedArtifactIds = [.../* @__PURE__ */ new Set([...run.generatedArtifactIds ?? [], ...generatedArtifacts.map((item) => item.id)])];
|
|
1284
|
+
run.sdkRepoUrl = input.sdkRepoUrl ?? run.sdkRepoUrl;
|
|
1285
|
+
run.sdkBranch = input.sdkBranch ?? run.sdkBranch;
|
|
1286
|
+
run.sdkCommitSha = input.sdkCommitSha ?? run.sdkCommitSha;
|
|
1287
|
+
run.sdkPreparedRoot = input.sdkPreparedRoot ?? run.sdkPreparedRoot;
|
|
1288
|
+
run.fingerprint = input.fingerprint;
|
|
1289
|
+
run.requesterUserId = input.actorUserId ?? run.requesterUserId;
|
|
1290
|
+
run.requesterPrincipalSubject = input.actorPrincipalSubject ?? run.requesterPrincipalSubject;
|
|
1291
|
+
run.requesterPrincipalScope = input.actorPrincipalScope ?? run.requesterPrincipalScope;
|
|
1292
|
+
run.startedAt = input.startedAt ?? run.startedAt;
|
|
1293
|
+
run.completedAt = input.completedAt ?? completedAt;
|
|
1294
|
+
issue.artifactIds = [...issue.artifactIds ?? [], ...generatedArtifacts.map((item) => item.id)];
|
|
1295
|
+
if (input.commentBody) {
|
|
1296
|
+
issue.commentIds = [...issue.commentIds ?? [], newVerificationComment(db, issue.id, input.commentBody, completedAt).id];
|
|
1297
|
+
}
|
|
1298
|
+
if (input.fingerprint) {
|
|
1299
|
+
issue.fingerprint = input.fingerprint;
|
|
1300
|
+
issue.fingerprints = upsertFingerprint(issue.fingerprints ?? [], input.fingerprint);
|
|
1301
|
+
}
|
|
1302
|
+
issue.updatedAt = completedAt;
|
|
1303
|
+
db.artifacts.push(...generatedArtifacts);
|
|
1304
|
+
return run;
|
|
1305
|
+
}
|
|
1306
|
+
function newVerificationComment(db, issueId, body, createdAt) {
|
|
1307
|
+
const comment = {
|
|
1308
|
+
id: randomUUID(),
|
|
1309
|
+
issueId,
|
|
1310
|
+
author: "Legato Worker",
|
|
1311
|
+
kind: "system",
|
|
1312
|
+
body,
|
|
1313
|
+
createdAt
|
|
1314
|
+
};
|
|
1315
|
+
db.comments.push(comment);
|
|
1316
|
+
return comment;
|
|
1317
|
+
}
|
|
1318
|
+
function verificationRunArtifactIds(artifacts, run) {
|
|
1319
|
+
const artifactIds = /* @__PURE__ */ new Set();
|
|
1320
|
+
if (run.logArtifactId) artifactIds.add(run.logArtifactId);
|
|
1321
|
+
if (run.generatedArtifactIds?.length) {
|
|
1322
|
+
for (const id of run.generatedArtifactIds) artifactIds.add(id);
|
|
1323
|
+
return [...artifactIds];
|
|
1324
|
+
}
|
|
1325
|
+
const expectedFilenames = new Set((run.generatedArtifacts ?? []).flatMap(generatedArtifactFilenameCandidates));
|
|
1326
|
+
if (expectedFilenames.size === 0) return [...artifactIds];
|
|
1327
|
+
for (const artifact of artifacts) {
|
|
1328
|
+
if (artifact.id === run.logArtifactId || artifact.kind !== "verification-output") continue;
|
|
1329
|
+
if (run.completedAt && artifact.createdAt !== run.completedAt) continue;
|
|
1330
|
+
if (expectedFilenames.has(artifact.filename) || [...expectedFilenames].some((filename) => artifact.filename.endsWith(filename))) {
|
|
1331
|
+
artifactIds.add(artifact.id);
|
|
1332
|
+
}
|
|
1333
|
+
}
|
|
1334
|
+
return [...artifactIds];
|
|
1335
|
+
}
|
|
1336
|
+
async function rewriteVerificationArtifact(artifact, text) {
|
|
1337
|
+
const bytes = Buffer.from(text, "utf8");
|
|
1338
|
+
await writeFile2(getArtifactTextPath(artifact), bytes);
|
|
1339
|
+
artifact.size = bytes.byteLength;
|
|
1340
|
+
artifact.checksum = createHash("sha256").update(bytes).digest("hex");
|
|
1341
|
+
}
|
|
1342
|
+
async function writeLocalVerificationArtifact(issueId, kind, logText, createdAt) {
|
|
1343
|
+
assertSafeIssueId(issueId);
|
|
1344
|
+
const bytes = Buffer.from(logText, "utf8");
|
|
1345
|
+
const id = randomUUID();
|
|
1346
|
+
const filename = verificationOutputFilename(kind, createdAt);
|
|
1347
|
+
const relativePath = path2.posix.join("artifacts", `${id}-${filename}`);
|
|
1348
|
+
await writeFile2(safeArtifactPath(dataRoot, relativePath), bytes);
|
|
1349
|
+
return verificationArtifact(id, filename, relativePath, bytes, createdAt);
|
|
1350
|
+
}
|
|
1351
|
+
async function writeLocalKernelSuggestionArtifact(input, createdAt) {
|
|
1352
|
+
const bytes = Buffer.from(input.content, "utf8");
|
|
1353
|
+
const id = randomUUID();
|
|
1354
|
+
const filename = sanitizeFilename(input.filename);
|
|
1355
|
+
const relativePath = path2.posix.join("artifacts", `${id}-${filename}`);
|
|
1356
|
+
await writeFile2(safeArtifactPath(dataRoot, relativePath), bytes);
|
|
1357
|
+
return kernelSuggestionArtifact(id, filename, relativePath, bytes, createdAt);
|
|
1358
|
+
}
|
|
1359
|
+
async function writeLocalVerificationGeneratedArtifacts(files, createdAt) {
|
|
1360
|
+
const artifacts = [];
|
|
1361
|
+
for (const file of files ?? []) {
|
|
1362
|
+
const bytes = Buffer.from(file.contentBase64, "base64");
|
|
1363
|
+
const id = randomUUID();
|
|
1364
|
+
const filename = sanitizeFilename(file.filename);
|
|
1365
|
+
const relativePath = path2.posix.join("artifacts", `${id}-${filename}`);
|
|
1366
|
+
await writeFile2(safeArtifactPath(dataRoot, relativePath), bytes);
|
|
1367
|
+
artifacts.push(verificationArtifact(id, filename, relativePath, bytes, createdAt, void 0, file.contentType));
|
|
1368
|
+
}
|
|
1369
|
+
return artifacts;
|
|
1370
|
+
}
|
|
1371
|
+
function verificationArtifact(id, filename, relativePath, bytes, createdAt, storage, contentType = "text/plain") {
|
|
1372
|
+
return {
|
|
1373
|
+
id,
|
|
1374
|
+
kind: "verification-output",
|
|
1375
|
+
filename,
|
|
1376
|
+
contentType,
|
|
1377
|
+
size: bytes.byteLength,
|
|
1378
|
+
checksum: createHash("sha256").update(bytes).digest("hex"),
|
|
1379
|
+
relativePath,
|
|
1380
|
+
storage,
|
|
1381
|
+
createdAt
|
|
1382
|
+
};
|
|
1383
|
+
}
|
|
1384
|
+
function verificationOutputFilename(kind, createdAt) {
|
|
1385
|
+
const compactTime = createdAt.replace(/[-:]/g, "").replace(/\.\d{3}Z$/, "Z");
|
|
1386
|
+
return sanitizeFilename(`verify-${kind}-${compactTime}.log`);
|
|
1387
|
+
}
|
|
1388
|
+
function kernelSuggestionArtifact(id, filename, relativePath, bytes, createdAt, storage) {
|
|
1389
|
+
return {
|
|
1390
|
+
id,
|
|
1391
|
+
kind: "kernel-suggestion",
|
|
1392
|
+
filename,
|
|
1393
|
+
contentType: "text/x-python",
|
|
1394
|
+
size: bytes.byteLength,
|
|
1395
|
+
checksum: createHash("sha256").update(bytes).digest("hex"),
|
|
1396
|
+
relativePath,
|
|
1397
|
+
storage,
|
|
1398
|
+
createdAt
|
|
1399
|
+
};
|
|
1400
|
+
}
|
|
1401
|
+
function kernelSuggestionComment(input, createdAt) {
|
|
1402
|
+
return {
|
|
1403
|
+
id: randomUUID(),
|
|
1404
|
+
issueId: input.issueId,
|
|
1405
|
+
author: input.author,
|
|
1406
|
+
authorUserId: input.authorUserId,
|
|
1407
|
+
authorPrincipalSubject: input.authorPrincipalSubject,
|
|
1408
|
+
authorPrincipalScope: input.authorPrincipalScope,
|
|
1409
|
+
kind: "system",
|
|
1410
|
+
body: `Kernel suggestion saved for ${input.filename}. Base artifact: ${input.baseArtifactId}.`,
|
|
1411
|
+
createdAt
|
|
1412
|
+
};
|
|
1413
|
+
}
|
|
1414
|
+
function kernelSuggestionUpdatedComment(input, createdAt) {
|
|
1415
|
+
return {
|
|
1416
|
+
id: randomUUID(),
|
|
1417
|
+
issueId: input.issueId,
|
|
1418
|
+
author: input.author,
|
|
1419
|
+
authorUserId: input.authorUserId,
|
|
1420
|
+
authorPrincipalSubject: input.authorPrincipalSubject,
|
|
1421
|
+
authorPrincipalScope: input.authorPrincipalScope,
|
|
1422
|
+
kind: "system",
|
|
1423
|
+
body: `Kernel suggestion updated for ${input.filename}. Artifact: ${input.suggestionArtifactId}.`,
|
|
1424
|
+
createdAt
|
|
1425
|
+
};
|
|
1426
|
+
}
|
|
1427
|
+
function statusChangeComment(input, previousStatus, createdAt) {
|
|
1428
|
+
return {
|
|
1429
|
+
id: randomUUID(),
|
|
1430
|
+
issueId: input.issueId,
|
|
1431
|
+
author: input.actor?.trim() || "Language Developer",
|
|
1432
|
+
authorUserId: input.actorUserId,
|
|
1433
|
+
authorPrincipalSubject: input.actorPrincipalSubject,
|
|
1434
|
+
authorPrincipalScope: input.actorPrincipalScope,
|
|
1435
|
+
kind: "system",
|
|
1436
|
+
body: `Status changed from ${previousStatus} to ${input.status}.`,
|
|
1437
|
+
createdAt
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
function assignmentComment(issueId, assignee, actor, createdAt) {
|
|
1441
|
+
return {
|
|
1442
|
+
id: randomUUID(),
|
|
1443
|
+
issueId,
|
|
1444
|
+
author: actor.trim() || "Language Developer",
|
|
1445
|
+
kind: "system",
|
|
1446
|
+
body: `Assigned to ${assignee}.`,
|
|
1447
|
+
createdAt
|
|
1448
|
+
};
|
|
1449
|
+
}
|
|
1450
|
+
function unassignmentComment(issueId, actor, createdAt, assignee) {
|
|
1451
|
+
return {
|
|
1452
|
+
id: randomUUID(),
|
|
1453
|
+
issueId,
|
|
1454
|
+
author: actor.trim() || "Language Developer",
|
|
1455
|
+
kind: "system",
|
|
1456
|
+
body: assignee ? `Unassigned ${assignee}.` : "Assignees cleared.",
|
|
1457
|
+
createdAt
|
|
1458
|
+
};
|
|
1459
|
+
}
|
|
1460
|
+
function blockerComment(issueId, blocker, actor, createdAt) {
|
|
1461
|
+
return {
|
|
1462
|
+
id: randomUUID(),
|
|
1463
|
+
issueId,
|
|
1464
|
+
author: actor.trim() || "Language Developer",
|
|
1465
|
+
kind: "system",
|
|
1466
|
+
body: `Blocker linked: ${blocker.repository}#${blocker.number} (${blocker.status}).`,
|
|
1467
|
+
createdAt
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function blockerSyncComment(issueId, blocker, actor, createdAt) {
|
|
1471
|
+
return {
|
|
1472
|
+
id: randomUUID(),
|
|
1473
|
+
issueId,
|
|
1474
|
+
author: actor.trim() || "Language Developer",
|
|
1475
|
+
kind: "system",
|
|
1476
|
+
body: `Blocker synced: ${blocker.repository}#${blocker.number} (${blocker.status}).`,
|
|
1477
|
+
createdAt
|
|
1478
|
+
};
|
|
1479
|
+
}
|
|
1480
|
+
function blockerRemovedComment(issueId, blocker, actor, createdAt) {
|
|
1481
|
+
return {
|
|
1482
|
+
id: randomUUID(),
|
|
1483
|
+
issueId,
|
|
1484
|
+
author: actor.trim() || "Language Developer",
|
|
1485
|
+
kind: "system",
|
|
1486
|
+
body: `Blocker removed: ${blocker.repository}#${blocker.number}.`,
|
|
1487
|
+
createdAt
|
|
1488
|
+
};
|
|
1489
|
+
}
|
|
1490
|
+
async function rewriteKernelSuggestionArtifact(artifact, content) {
|
|
1491
|
+
const bytes = Buffer.from(content, "utf8");
|
|
1492
|
+
await writeFile2(getArtifactTextPath(artifact), bytes);
|
|
1493
|
+
return {
|
|
1494
|
+
...artifact,
|
|
1495
|
+
size: bytes.byteLength,
|
|
1496
|
+
checksum: createHash("sha256").update(bytes).digest("hex")
|
|
1497
|
+
};
|
|
1498
|
+
}
|
|
1499
|
+
function verificationComment(run, filename) {
|
|
1500
|
+
const exit = typeof run.exitCode === "number" ? ` exit ${run.exitCode}` : " no exit code";
|
|
1501
|
+
return `Verification ${run.kind} ${run.status}.${exit}. Log artifact: ${filename}.`;
|
|
1502
|
+
}
|
|
1503
|
+
var portableIssuesCache;
|
|
1504
|
+
async function readPortableIssues() {
|
|
1505
|
+
try {
|
|
1506
|
+
const root = await readableIssueStoreRoot();
|
|
1507
|
+
const issuesRoot = safeJoin(root, "issues");
|
|
1508
|
+
const entries = await readdir(issuesRoot, { withFileTypes: true });
|
|
1509
|
+
const issueNames = entries.filter((entry) => entry.isDirectory()).filter((entry) => /^[a-zA-Z0-9][a-zA-Z0-9_-]{0,127}$/.test(entry.name)).map((entry) => entry.name).sort();
|
|
1510
|
+
const signature = await portableIssuesSignature(root, issueNames);
|
|
1511
|
+
if (portableIssuesCache && portableIssuesCache.signature === signature) {
|
|
1512
|
+
return portableIssuesCache.records;
|
|
1513
|
+
}
|
|
1514
|
+
const records = (await Promise.all(
|
|
1515
|
+
issueNames.map((name) => readPortableIssue(name, { root }))
|
|
1516
|
+
)).filter(Boolean);
|
|
1517
|
+
portableIssuesCache = { signature, records };
|
|
1518
|
+
return records;
|
|
1519
|
+
} catch {
|
|
1520
|
+
return [];
|
|
1521
|
+
}
|
|
1522
|
+
}
|
|
1523
|
+
async function portableIssuesSignature(root, issueNames) {
|
|
1524
|
+
const parts = await Promise.all(issueNames.map(async (name) => {
|
|
1525
|
+
try {
|
|
1526
|
+
const details = await stat(safeJoin(root, "issues", name, "issue.json"));
|
|
1527
|
+
return `${name}:${details.mtimeMs}:${details.size}`;
|
|
1528
|
+
} catch {
|
|
1529
|
+
return `${name}:missing`;
|
|
1530
|
+
}
|
|
1531
|
+
}));
|
|
1532
|
+
return `${path2.resolve(root)}
|
|
1533
|
+
${parts.join("\n")}`;
|
|
1534
|
+
}
|
|
1535
|
+
async function readPortableIssue(id, options = {}) {
|
|
1536
|
+
try {
|
|
1537
|
+
const root = options.root ?? await readableIssueStoreRoot();
|
|
1538
|
+
assertSafeIssueId(id);
|
|
1539
|
+
const issueDir = safeJoin(root, "issues", id);
|
|
1540
|
+
const raw = await readFile2(safeJoin(issueDir, "issue.json"), "utf8");
|
|
1541
|
+
const portable = JSON.parse(raw);
|
|
1542
|
+
if (portable.id !== id) return null;
|
|
1543
|
+
if (portable.duplicateOf) {
|
|
1544
|
+
return options.followDuplicate ? readPortableIssue(portable.duplicateOf, options) : null;
|
|
1545
|
+
}
|
|
1546
|
+
assertSafeIssueId(portable.id);
|
|
1547
|
+
const description = await readOptionalFile(safeJoin(issueDir, "description.md"));
|
|
1548
|
+
const createdAt = portable.createdAt;
|
|
1549
|
+
const creatorVendorId = normalizeVendorId(portable.creatorVendorId);
|
|
1550
|
+
const kernelArtifact = await portableArtifact(root, portable.id, "kernel", portable.kernel.filename, portable.kernel.checksum);
|
|
1551
|
+
const artifacts = [kernelArtifact];
|
|
1552
|
+
let errorLogArtifact;
|
|
1553
|
+
if (portable.log?.filename) {
|
|
1554
|
+
errorLogArtifact = await portableArtifact(root, portable.id, "error-log", portable.log.filename);
|
|
1555
|
+
artifacts.push(errorLogArtifact);
|
|
1556
|
+
}
|
|
1557
|
+
const sidecarArtifacts = (portable.artifacts ?? []).filter((artifact) => isSafePortableSidecarArtifact(portable.id, artifact));
|
|
1558
|
+
artifacts.push(...sidecarArtifacts);
|
|
1559
|
+
const bundle = {
|
|
1560
|
+
id: `${portable.id}-bundle`,
|
|
1561
|
+
source: portable.source ?? "shell",
|
|
1562
|
+
kernelArtifactId: kernelArtifact.id,
|
|
1563
|
+
errorLogArtifactId: errorLogArtifact?.id,
|
|
1564
|
+
legatoBinaryVersion: portable.legatoBinaryVersion,
|
|
1565
|
+
cliVersion: portable.cliVersion,
|
|
1566
|
+
gitMetadata: portable.gitMetadata,
|
|
1567
|
+
environmentMetadata: portable.environmentMetadata,
|
|
1568
|
+
createdAt
|
|
1569
|
+
};
|
|
1570
|
+
const primaryOccurrence = {
|
|
1571
|
+
id: `${portable.id}-primary-occurrence`,
|
|
1572
|
+
issueId: portable.id,
|
|
1573
|
+
reporter: portable.reporter,
|
|
1574
|
+
reporterUserId: portable.reporterUserId,
|
|
1575
|
+
reporterPrincipalSubject: portable.reporterPrincipalSubject,
|
|
1576
|
+
reporterPrincipalScope: portable.reporterPrincipalScope,
|
|
1577
|
+
reporterVendorId: creatorVendorId,
|
|
1578
|
+
bundleId: bundle.id,
|
|
1579
|
+
artifactIds: artifacts.map((artifact) => artifact.id),
|
|
1580
|
+
kernelChecksum: kernelArtifact.checksum,
|
|
1581
|
+
source: bundle.source,
|
|
1582
|
+
gitMetadata: bundle.gitMetadata,
|
|
1583
|
+
environmentMetadata: bundle.environmentMetadata,
|
|
1584
|
+
createdAt
|
|
1585
|
+
};
|
|
1586
|
+
const occurrences = (portable.occurrences?.length ? portable.occurrences : [primaryOccurrence]).map((occurrence) => hydrateOccurrence(occurrence, creatorVendorId));
|
|
1587
|
+
const systemComment = {
|
|
1588
|
+
id: `${portable.id}-system-comment`,
|
|
1589
|
+
issueId: portable.id,
|
|
1590
|
+
author: "Legato Agora",
|
|
1591
|
+
kind: "system",
|
|
1592
|
+
body: "Portable issue bundle loaded from issue-store. Lowering and fsim automation will be attached in M2.",
|
|
1593
|
+
createdAt
|
|
1594
|
+
};
|
|
1595
|
+
const issue = {
|
|
1596
|
+
schemaVersion: portable.schemaVersion ?? 0,
|
|
1597
|
+
id: portable.id,
|
|
1598
|
+
title: portable.title,
|
|
1599
|
+
description: description || `Reported from portable issue bundle with kernel ${portable.kernel.filename}.`,
|
|
1600
|
+
status: portable.status ?? "open",
|
|
1601
|
+
creator: portable.reporter,
|
|
1602
|
+
creatorUserId: portable.reporterUserId,
|
|
1603
|
+
creatorPrincipalSubject: portable.reporterPrincipalSubject,
|
|
1604
|
+
creatorPrincipalScope: portable.reporterPrincipalScope,
|
|
1605
|
+
creatorVendorId,
|
|
1606
|
+
currentBundleId: bundle.id,
|
|
1607
|
+
artifactIds: artifacts.map((artifact) => artifact.id),
|
|
1608
|
+
commentIds: [systemComment.id, ...(portable.comments ?? []).map((comment) => comment.id)],
|
|
1609
|
+
verificationRunIds: (portable.verificationRuns ?? []).map((run) => run.id),
|
|
1610
|
+
fingerprint: portable.fingerprint,
|
|
1611
|
+
fingerprints: portable.fingerprints ?? (portable.fingerprint ? [portable.fingerprint] : []),
|
|
1612
|
+
occurrenceCount: portable.occurrenceCount ?? Math.max(1, occurrences.length),
|
|
1613
|
+
occurrenceIds: occurrences.map((occurrence) => occurrence.id),
|
|
1614
|
+
assignee: firstAssignee(issueAssignees(portable)),
|
|
1615
|
+
assignees: issueAssignees(portable),
|
|
1616
|
+
blockers: portable.blockers ?? [],
|
|
1617
|
+
github: portable.github,
|
|
1618
|
+
store: portable.store ?? {
|
|
1619
|
+
bundlePath: path2.posix.join("issues", portable.id)
|
|
1620
|
+
},
|
|
1621
|
+
tags: uniqueTags2([
|
|
1622
|
+
...portable.tags ?? [],
|
|
1623
|
+
...deriveTags({
|
|
1624
|
+
title: portable.title,
|
|
1625
|
+
description,
|
|
1626
|
+
kernelFilename: portable.kernel.filename,
|
|
1627
|
+
hasLog: Boolean(portable.log?.filename),
|
|
1628
|
+
source: portable.source ?? "shell"
|
|
1629
|
+
}),
|
|
1630
|
+
...IssueBlockerTags.merge([], portable.blockers ?? [])
|
|
1631
|
+
]),
|
|
1632
|
+
createdAt,
|
|
1633
|
+
updatedAt: portable.updatedAt
|
|
1634
|
+
};
|
|
1635
|
+
return {
|
|
1636
|
+
issue,
|
|
1637
|
+
bundle,
|
|
1638
|
+
artifacts,
|
|
1639
|
+
comments: [systemComment, ...portable.comments ?? []],
|
|
1640
|
+
verificationRuns: portable.verificationRuns ?? [],
|
|
1641
|
+
occurrences
|
|
1642
|
+
};
|
|
1643
|
+
} catch {
|
|
1644
|
+
return null;
|
|
1645
|
+
}
|
|
1646
|
+
}
|
|
1647
|
+
async function recordPortableVerificationResult(input) {
|
|
1648
|
+
assertSafeIssueId(input.issueId);
|
|
1649
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1650
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1651
|
+
const portable = JSON.parse(raw);
|
|
1652
|
+
if (portable.id !== input.issueId) {
|
|
1653
|
+
throw new Error(`Portable issue id mismatch: ${input.issueId}`);
|
|
1654
|
+
}
|
|
1655
|
+
assertSafeIssueId(portable.id);
|
|
1656
|
+
const completedAt = input.completedAt ?? now();
|
|
1657
|
+
const startedAt = input.startedAt ?? completedAt;
|
|
1658
|
+
const artifact = await writePortableVerificationArtifact(portable.id, input.kind, input.logText, completedAt);
|
|
1659
|
+
const generatedArtifacts = await writePortableVerificationGeneratedArtifacts(portable.id, input.generatedArtifactFiles, completedAt);
|
|
1660
|
+
const bundleId = `${portable.id}-bundle`;
|
|
1661
|
+
const run = {
|
|
1662
|
+
id: randomUUID(),
|
|
1663
|
+
issueId: portable.id,
|
|
1664
|
+
bundleId,
|
|
1665
|
+
sourceArtifactId: input.sourceArtifactId,
|
|
1666
|
+
kind: input.kind,
|
|
1667
|
+
status: input.status,
|
|
1668
|
+
exitCode: input.exitCode,
|
|
1669
|
+
logArtifactId: artifact.id,
|
|
1670
|
+
toolVersion: input.toolVersion,
|
|
1671
|
+
failedStage: input.failedStage,
|
|
1672
|
+
completedStage: input.completedStage,
|
|
1673
|
+
generatedArtifacts: input.generatedArtifacts,
|
|
1674
|
+
generatedArtifactIds: generatedArtifacts.map((item) => item.id),
|
|
1675
|
+
sdkRepoUrl: input.sdkRepoUrl,
|
|
1676
|
+
sdkBranch: input.sdkBranch,
|
|
1677
|
+
sdkCommitSha: input.sdkCommitSha,
|
|
1678
|
+
sdkPreparedRoot: input.sdkPreparedRoot,
|
|
1679
|
+
fingerprint: input.fingerprint,
|
|
1680
|
+
startedAt,
|
|
1681
|
+
completedAt,
|
|
1682
|
+
requesterUserId: input.actorUserId,
|
|
1683
|
+
requesterPrincipalSubject: input.actorPrincipalSubject,
|
|
1684
|
+
requesterPrincipalScope: input.actorPrincipalScope
|
|
1685
|
+
};
|
|
1686
|
+
const comment = {
|
|
1687
|
+
id: randomUUID(),
|
|
1688
|
+
issueId: portable.id,
|
|
1689
|
+
author: "Legato Worker",
|
|
1690
|
+
kind: "system",
|
|
1691
|
+
body: input.commentBody ?? verificationComment(run, artifact.filename),
|
|
1692
|
+
createdAt: completedAt
|
|
1693
|
+
};
|
|
1694
|
+
portable.artifacts = [...portable.artifacts ?? [], artifact, ...generatedArtifacts];
|
|
1695
|
+
portable.verificationRuns = [...portable.verificationRuns ?? [], run];
|
|
1696
|
+
portable.comments = [...portable.comments ?? [], comment];
|
|
1697
|
+
if (input.fingerprint) {
|
|
1698
|
+
portable.fingerprint = input.fingerprint;
|
|
1699
|
+
portable.fingerprints = upsertFingerprint(portable.fingerprints ?? [], input.fingerprint);
|
|
1700
|
+
}
|
|
1701
|
+
portable.updatedAt = completedAt;
|
|
1702
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1703
|
+
return run;
|
|
1704
|
+
}
|
|
1705
|
+
async function updatePortableVerificationRun(input) {
|
|
1706
|
+
assertSafeIssueId(input.issueId);
|
|
1707
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1708
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1709
|
+
const portable = JSON.parse(raw);
|
|
1710
|
+
if (portable.id !== input.issueId) {
|
|
1711
|
+
throw new Error(`Portable issue id mismatch: ${input.issueId}`);
|
|
1712
|
+
}
|
|
1713
|
+
assertSafeIssueId(portable.id);
|
|
1714
|
+
const completedAt = input.completedAt ?? now();
|
|
1715
|
+
const runs = portable.verificationRuns ?? [];
|
|
1716
|
+
const runIndex = runs.findIndex((item) => item.id === input.runId);
|
|
1717
|
+
if (runIndex < 0) throw new Error(`Verification run not found: ${input.runId}`);
|
|
1718
|
+
const currentRun = runs[runIndex];
|
|
1719
|
+
const artifacts = portable.artifacts ?? [];
|
|
1720
|
+
const artifactIndex = artifacts.findIndex((item) => item.id === currentRun.logArtifactId);
|
|
1721
|
+
if (artifactIndex < 0) throw new Error(`Verification log artifact not found for run: ${input.runId}`);
|
|
1722
|
+
const updatedArtifact = await rewritePortableVerificationArtifact(artifacts[artifactIndex], input.logText);
|
|
1723
|
+
const generatedArtifacts = await writePortableVerificationGeneratedArtifacts(portable.id, input.generatedArtifactFiles, completedAt);
|
|
1724
|
+
const updatedRun = {
|
|
1725
|
+
...currentRun,
|
|
1726
|
+
sourceArtifactId: input.sourceArtifactId ?? currentRun.sourceArtifactId,
|
|
1727
|
+
status: input.status,
|
|
1728
|
+
exitCode: input.exitCode,
|
|
1729
|
+
toolVersion: input.toolVersion,
|
|
1730
|
+
failedStage: input.failedStage,
|
|
1731
|
+
completedStage: input.completedStage,
|
|
1732
|
+
generatedArtifacts: input.generatedArtifacts,
|
|
1733
|
+
generatedArtifactIds: generatedArtifacts.length > 0 ? [.../* @__PURE__ */ new Set([...currentRun.generatedArtifactIds ?? [], ...generatedArtifacts.map((item) => item.id)])] : currentRun.generatedArtifactIds,
|
|
1734
|
+
sdkRepoUrl: input.sdkRepoUrl ?? currentRun.sdkRepoUrl,
|
|
1735
|
+
sdkBranch: input.sdkBranch ?? currentRun.sdkBranch,
|
|
1736
|
+
sdkCommitSha: input.sdkCommitSha ?? currentRun.sdkCommitSha,
|
|
1737
|
+
sdkPreparedRoot: input.sdkPreparedRoot ?? currentRun.sdkPreparedRoot,
|
|
1738
|
+
fingerprint: input.fingerprint,
|
|
1739
|
+
requesterUserId: input.actorUserId ?? currentRun.requesterUserId,
|
|
1740
|
+
requesterPrincipalSubject: input.actorPrincipalSubject ?? currentRun.requesterPrincipalSubject,
|
|
1741
|
+
requesterPrincipalScope: input.actorPrincipalScope ?? currentRun.requesterPrincipalScope,
|
|
1742
|
+
startedAt: input.startedAt ?? currentRun.startedAt,
|
|
1743
|
+
// For running updates this doubles as a liveness heartbeat so orphaned runs can be detected.
|
|
1744
|
+
completedAt: input.completedAt ?? completedAt
|
|
1745
|
+
};
|
|
1746
|
+
const nextRuns = [...runs];
|
|
1747
|
+
nextRuns[runIndex] = updatedRun;
|
|
1748
|
+
const nextArtifacts = [...artifacts];
|
|
1749
|
+
nextArtifacts[artifactIndex] = updatedArtifact;
|
|
1750
|
+
portable.artifacts = [...nextArtifacts, ...generatedArtifacts];
|
|
1751
|
+
portable.verificationRuns = nextRuns;
|
|
1752
|
+
if (input.commentBody) {
|
|
1753
|
+
portable.comments = [...portable.comments ?? [], {
|
|
1754
|
+
id: randomUUID(),
|
|
1755
|
+
issueId: portable.id,
|
|
1756
|
+
author: "Legato Worker",
|
|
1757
|
+
kind: "system",
|
|
1758
|
+
body: input.commentBody,
|
|
1759
|
+
createdAt: completedAt
|
|
1760
|
+
}];
|
|
1761
|
+
}
|
|
1762
|
+
if (input.fingerprint) {
|
|
1763
|
+
portable.fingerprint = input.fingerprint;
|
|
1764
|
+
portable.fingerprints = upsertFingerprint(portable.fingerprints ?? [], input.fingerprint);
|
|
1765
|
+
}
|
|
1766
|
+
portable.updatedAt = completedAt;
|
|
1767
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1768
|
+
return updatedRun;
|
|
1769
|
+
}
|
|
1770
|
+
async function deletePortableVerificationRuns(issueId, runIds) {
|
|
1771
|
+
assertSafeIssueId(issueId);
|
|
1772
|
+
const issuePath = await portableIssuePathFollowingDuplicate(issueId);
|
|
1773
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1774
|
+
const portable = JSON.parse(raw);
|
|
1775
|
+
if (portable.id !== issueId) {
|
|
1776
|
+
throw new Error(`Portable issue id mismatch: ${issueId}`);
|
|
1777
|
+
}
|
|
1778
|
+
const selectedRunIds = new Set(runIds);
|
|
1779
|
+
const runs = portable.verificationRuns ?? [];
|
|
1780
|
+
const selectedRuns = runs.filter((run) => selectedRunIds.has(run.id));
|
|
1781
|
+
const deletedRunIds = selectedRuns.map((run) => run.id);
|
|
1782
|
+
if (deletedRunIds.length === 0) return { deletedRunIds: [] };
|
|
1783
|
+
const artifacts = portable.artifacts ?? [];
|
|
1784
|
+
const artifactIds = new Set(selectedRuns.flatMap((run) => verificationRunArtifactIds(artifacts, run)));
|
|
1785
|
+
const deletedArtifacts = artifacts.filter((artifact) => artifactIds.has(artifact.id));
|
|
1786
|
+
portable.verificationRuns = runs.filter((run) => !selectedRunIds.has(run.id));
|
|
1787
|
+
portable.artifacts = artifacts.filter((artifact) => !artifactIds.has(artifact.id));
|
|
1788
|
+
portable.updatedAt = now();
|
|
1789
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1790
|
+
await Promise.all(deletedArtifacts.map((artifact) => rm(safeArtifactPath(issueStoreRoot, artifact.relativePath), { force: true })));
|
|
1791
|
+
return { deletedRunIds };
|
|
1792
|
+
}
|
|
1793
|
+
async function addPortableKernelSuggestion(input) {
|
|
1794
|
+
assertSafeIssueId(input.issueId);
|
|
1795
|
+
assertSafeFilename(input.filename);
|
|
1796
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1797
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1798
|
+
const portable = JSON.parse(raw);
|
|
1799
|
+
if (portable.id !== input.issueId) {
|
|
1800
|
+
throw new Error(`Portable issue id mismatch: ${input.issueId}`);
|
|
1801
|
+
}
|
|
1802
|
+
const createdAt = now();
|
|
1803
|
+
const artifact = await writePortableKernelSuggestionArtifact(portable.id, input.filename, input.content, createdAt);
|
|
1804
|
+
portable.artifacts = [...portable.artifacts ?? [], artifact];
|
|
1805
|
+
portable.comments = [...portable.comments ?? [], kernelSuggestionComment(input, createdAt)];
|
|
1806
|
+
portable.updatedAt = createdAt;
|
|
1807
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1808
|
+
return artifact;
|
|
1809
|
+
}
|
|
1810
|
+
async function updatePortableKernelSuggestion(input) {
|
|
1811
|
+
assertSafeIssueId(input.issueId);
|
|
1812
|
+
assertSafeFilename(input.filename);
|
|
1813
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1814
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1815
|
+
const portable = JSON.parse(raw);
|
|
1816
|
+
if (portable.id !== input.issueId) {
|
|
1817
|
+
throw new Error(`Portable issue id mismatch: ${input.issueId}`);
|
|
1818
|
+
}
|
|
1819
|
+
const artifactIndex = (portable.artifacts ?? []).findIndex((artifact2) => artifact2.id === input.suggestionArtifactId);
|
|
1820
|
+
const artifact = artifactIndex >= 0 ? portable.artifacts?.[artifactIndex] : void 0;
|
|
1821
|
+
if (!artifact || artifact.kind !== "kernel-suggestion") {
|
|
1822
|
+
throw new Error(`Kernel suggestion not found: ${input.suggestionArtifactId}`);
|
|
1823
|
+
}
|
|
1824
|
+
const updatedAt = now();
|
|
1825
|
+
const updatedArtifact = await rewriteKernelSuggestionArtifact(artifact, input.content);
|
|
1826
|
+
const artifacts = [...portable.artifacts ?? []];
|
|
1827
|
+
artifacts[artifactIndex] = updatedArtifact;
|
|
1828
|
+
portable.artifacts = artifacts;
|
|
1829
|
+
portable.comments = [...portable.comments ?? [], kernelSuggestionUpdatedComment(input, updatedAt)];
|
|
1830
|
+
portable.updatedAt = updatedAt;
|
|
1831
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1832
|
+
return updatedArtifact;
|
|
1833
|
+
}
|
|
1834
|
+
async function portableIssuePathFollowingDuplicate(issueId) {
|
|
1835
|
+
assertSafeIssueId(issueId);
|
|
1836
|
+
const issuePath = safeJoin(issueStoreRoot, "issues", issueId, "issue.json");
|
|
1837
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1838
|
+
const portable = JSON.parse(raw);
|
|
1839
|
+
if (portable.id !== issueId) {
|
|
1840
|
+
throw new Error(`Portable issue id mismatch: ${issueId}`);
|
|
1841
|
+
}
|
|
1842
|
+
if (!portable.duplicateOf) return issuePath;
|
|
1843
|
+
assertSafeIssueId(portable.duplicateOf);
|
|
1844
|
+
return safeJoin(issueStoreRoot, "issues", portable.duplicateOf, "issue.json");
|
|
1845
|
+
}
|
|
1846
|
+
async function writePortableVerificationArtifact(issueId, kind, logText, createdAt) {
|
|
1847
|
+
assertSafeIssueId(issueId);
|
|
1848
|
+
const bytes = Buffer.from(logText, "utf8");
|
|
1849
|
+
const id = randomUUID();
|
|
1850
|
+
const filename = verificationOutputFilename(kind, createdAt);
|
|
1851
|
+
const relativePath = path2.posix.join("issues", issueId, "artifacts", filename);
|
|
1852
|
+
await mkdir2(safeJoin(issueStoreRoot, "issues", issueId, "artifacts"), { recursive: true });
|
|
1853
|
+
await writeFile2(safeArtifactPath(issueStoreRoot, relativePath), bytes);
|
|
1854
|
+
return verificationArtifact(id, filename, relativePath, bytes, createdAt, "issue-store");
|
|
1855
|
+
}
|
|
1856
|
+
async function writePortableKernelSuggestionArtifact(issueId, filename, content, createdAt) {
|
|
1857
|
+
assertSafeIssueId(issueId);
|
|
1858
|
+
const bytes = Buffer.from(content, "utf8");
|
|
1859
|
+
const id = randomUUID();
|
|
1860
|
+
const timestamp = createdAt.replace(/[:.]/g, "-");
|
|
1861
|
+
const safeFilename = sanitizeFilename(`${timestamp}-suggestion-${filename}`);
|
|
1862
|
+
const relativePath = path2.posix.join("issues", issueId, "artifacts", safeFilename);
|
|
1863
|
+
await mkdir2(safeJoin(issueStoreRoot, "issues", issueId, "artifacts"), { recursive: true });
|
|
1864
|
+
await writeFile2(safeArtifactPath(issueStoreRoot, relativePath), bytes);
|
|
1865
|
+
return kernelSuggestionArtifact(id, safeFilename, relativePath, bytes, createdAt, "issue-store");
|
|
1866
|
+
}
|
|
1867
|
+
async function rewritePortableVerificationArtifact(artifact, text) {
|
|
1868
|
+
const bytes = Buffer.from(text, "utf8");
|
|
1869
|
+
await writeFile2(safeArtifactPath(issueStoreRoot, artifact.relativePath), bytes);
|
|
1870
|
+
return {
|
|
1871
|
+
...artifact,
|
|
1872
|
+
size: bytes.byteLength,
|
|
1873
|
+
checksum: createHash("sha256").update(bytes).digest("hex")
|
|
1874
|
+
};
|
|
1875
|
+
}
|
|
1876
|
+
async function writePortableVerificationGeneratedArtifacts(issueId, files, createdAt) {
|
|
1877
|
+
assertSafeIssueId(issueId);
|
|
1878
|
+
const artifacts = [];
|
|
1879
|
+
const artifactDir = safeJoin(issueStoreRoot, "issues", issueId, "artifacts");
|
|
1880
|
+
const timestamp = createdAt.replace(/[:.]/g, "-");
|
|
1881
|
+
await mkdir2(artifactDir, { recursive: true });
|
|
1882
|
+
for (const file of files ?? []) {
|
|
1883
|
+
const bytes = Buffer.from(file.contentBase64, "base64");
|
|
1884
|
+
const id = randomUUID();
|
|
1885
|
+
const filename = sanitizeFilename(`${timestamp}-${file.filename}`);
|
|
1886
|
+
const relativePath = path2.posix.join("issues", issueId, "artifacts", filename);
|
|
1887
|
+
await writeFile2(safeArtifactPath(issueStoreRoot, relativePath), bytes);
|
|
1888
|
+
artifacts.push(verificationArtifact(id, filename, relativePath, bytes, createdAt, "issue-store", file.contentType));
|
|
1889
|
+
}
|
|
1890
|
+
return artifacts;
|
|
1891
|
+
}
|
|
1892
|
+
async function updatePortableIssueStatus(input, updatedAt) {
|
|
1893
|
+
assertSafeIssueId(input.issueId);
|
|
1894
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1895
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1896
|
+
const portable = JSON.parse(raw);
|
|
1897
|
+
const previousStatus = portable.status ?? "open";
|
|
1898
|
+
portable.status = input.status;
|
|
1899
|
+
portable.comments = [...portable.comments ?? [], statusChangeComment({ ...input, issueId: portable.id }, previousStatus, updatedAt)];
|
|
1900
|
+
portable.updatedAt = updatedAt;
|
|
1901
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1902
|
+
}
|
|
1903
|
+
async function updatePortableIssueDescription(input, updatedAt) {
|
|
1904
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1905
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1906
|
+
const portable = JSON.parse(raw);
|
|
1907
|
+
if (!canMutateIssueDescription(portable.reporter, portable.reporterUserId, input.actor, input.actorUserId, input.actorPrincipalSubject, input.actorPrincipalScope, input.authPolicy)) {
|
|
1908
|
+
throw new Error("issue_description_forbidden");
|
|
1909
|
+
}
|
|
1910
|
+
portable.updatedAt = updatedAt;
|
|
1911
|
+
await writeTextAtomic(safeJoin(path2.dirname(issuePath), "description.md"), input.description.trim());
|
|
1912
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1913
|
+
}
|
|
1914
|
+
function canMutateIssueDescription(owner, ownerUserId, actor, actorUserId, actorPrincipalSubject, actorPrincipalScope, authPolicy) {
|
|
1915
|
+
if (actorUserId && actorPrincipalSubject && actorPrincipalScope === "admin") return true;
|
|
1916
|
+
if (ownerUserId && actorUserId && ownerUserId === actorUserId) return true;
|
|
1917
|
+
if (isSameTenantPrincipalLegacyOwner(ownerUserId, actorUserId, actorPrincipalSubject)) return true;
|
|
1918
|
+
if (ownerUserId) return false;
|
|
1919
|
+
if (authPolicy?.enforceTenantIdentity) return false;
|
|
1920
|
+
return owner.trim().toLowerCase() === actor.trim().toLowerCase();
|
|
1921
|
+
}
|
|
1922
|
+
async function addPortableIssueTags(id, tags, updatedAt) {
|
|
1923
|
+
assertSafeIssueId(id);
|
|
1924
|
+
const issuePath = await portableIssuePathFollowingDuplicate(id);
|
|
1925
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1926
|
+
const portable = JSON.parse(raw);
|
|
1927
|
+
portable.tags = uniqueTags2([...portable.tags ?? [], ...tags]);
|
|
1928
|
+
portable.updatedAt = updatedAt;
|
|
1929
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1930
|
+
}
|
|
1931
|
+
async function assignPortableIssue(input, updatedAt) {
|
|
1932
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1933
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1934
|
+
const portable = JSON.parse(raw);
|
|
1935
|
+
const assignee = new IssueAssigneeName(input.assignee).value;
|
|
1936
|
+
const nextAssignee = IssueAssignment.create({
|
|
1937
|
+
assignee,
|
|
1938
|
+
assigneeUserId: input.assigneeUserId,
|
|
1939
|
+
actor: input.actor,
|
|
1940
|
+
actorUserId: input.actorUserId,
|
|
1941
|
+
actorPrincipalSubject: input.actorPrincipalSubject,
|
|
1942
|
+
actorPrincipalScope: input.actorPrincipalScope,
|
|
1943
|
+
assignedAt: updatedAt
|
|
1944
|
+
});
|
|
1945
|
+
const assignees = [
|
|
1946
|
+
...issueAssignees(portable).filter((item) => item.name.toLowerCase() !== nextAssignee.name.toLowerCase()),
|
|
1947
|
+
nextAssignee
|
|
1948
|
+
];
|
|
1949
|
+
portable.assignees = assignees;
|
|
1950
|
+
portable.assignee = firstAssignee(assignees);
|
|
1951
|
+
portable.comments = [...portable.comments ?? [], assignmentComment(portable.id, assignee, input.actor, updatedAt)];
|
|
1952
|
+
portable.updatedAt = updatedAt;
|
|
1953
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1954
|
+
}
|
|
1955
|
+
async function unassignPortableIssue(input, updatedAt) {
|
|
1956
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1957
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1958
|
+
const portable = JSON.parse(raw);
|
|
1959
|
+
const assignee = new IssueAssigneeName(input.assignee).value;
|
|
1960
|
+
const assignees = issueAssignees(portable).filter((item) => item.name.toLowerCase() !== assignee.toLowerCase());
|
|
1961
|
+
portable.assignees = assignees;
|
|
1962
|
+
portable.assignee = firstAssignee(assignees);
|
|
1963
|
+
portable.comments = [...portable.comments ?? [], unassignmentComment(portable.id, input.actor, updatedAt, assignee)];
|
|
1964
|
+
portable.updatedAt = updatedAt;
|
|
1965
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1966
|
+
}
|
|
1967
|
+
async function clearPortableIssueAssignee(issueId, actor, updatedAt) {
|
|
1968
|
+
const issuePath = await portableIssuePathFollowingDuplicate(issueId);
|
|
1969
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1970
|
+
const portable = JSON.parse(raw);
|
|
1971
|
+
portable.assignees = [];
|
|
1972
|
+
portable.assignee = void 0;
|
|
1973
|
+
portable.comments = [...portable.comments ?? [], unassignmentComment(portable.id, actor, updatedAt)];
|
|
1974
|
+
portable.updatedAt = updatedAt;
|
|
1975
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1976
|
+
}
|
|
1977
|
+
async function addPortableIssueBlocker(issueId, blocker, actor, updatedAt) {
|
|
1978
|
+
const issuePath = await portableIssuePathFollowingDuplicate(issueId);
|
|
1979
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1980
|
+
const portable = JSON.parse(raw);
|
|
1981
|
+
const blockers = [...(portable.blockers ?? []).filter((item) => item.repository !== blocker.repository || item.number !== blocker.number), blocker];
|
|
1982
|
+
portable.blockers = blockers;
|
|
1983
|
+
portable.tags = IssueBlockerTags.merge(portable.tags ?? [], blockers);
|
|
1984
|
+
portable.comments = [...portable.comments ?? [], blockerComment(portable.id, blocker, actor, updatedAt)];
|
|
1985
|
+
portable.updatedAt = updatedAt;
|
|
1986
|
+
await writePortableIssueJson(issuePath, portable);
|
|
1987
|
+
}
|
|
1988
|
+
async function syncPortableIssueBlocker(input) {
|
|
1989
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
1990
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
1991
|
+
const portable = JSON.parse(raw);
|
|
1992
|
+
const blocker = (portable.blockers ?? []).find((item) => item.id === input.blockerId);
|
|
1993
|
+
if (!blocker) throw new Error(`Blocker not found: ${input.blockerId}`);
|
|
1994
|
+
const synced = await new GithubPullRequestSyncService().sync(blocker);
|
|
1995
|
+
const updatedAt = synced.lastSyncedAt ?? now();
|
|
1996
|
+
const blockers = (portable.blockers ?? []).map((item) => item.id === synced.id ? synced : item);
|
|
1997
|
+
portable.blockers = blockers;
|
|
1998
|
+
portable.tags = IssueBlockerTags.merge(portable.tags ?? [], blockers);
|
|
1999
|
+
portable.comments = [...portable.comments ?? [], blockerSyncComment(portable.id, synced, input.actor, updatedAt)];
|
|
2000
|
+
portable.updatedAt = updatedAt;
|
|
2001
|
+
await writePortableIssueJson(issuePath, portable);
|
|
2002
|
+
return synced;
|
|
2003
|
+
}
|
|
2004
|
+
async function removePortableIssueBlocker(input, updatedAt) {
|
|
2005
|
+
const issuePath = await portableIssuePathFollowingDuplicate(input.issueId);
|
|
2006
|
+
const raw = await readFile2(issuePath, "utf8");
|
|
2007
|
+
const portable = JSON.parse(raw);
|
|
2008
|
+
const blocker = (portable.blockers ?? []).find((item) => item.id === input.blockerId);
|
|
2009
|
+
if (!blocker) throw new Error(`Blocker not found: ${input.blockerId}`);
|
|
2010
|
+
const blockers = (portable.blockers ?? []).filter((item) => item.id !== input.blockerId);
|
|
2011
|
+
portable.blockers = blockers;
|
|
2012
|
+
portable.tags = IssueBlockerTags.merge(portable.tags ?? [], blockers);
|
|
2013
|
+
portable.comments = [...portable.comments ?? [], blockerRemovedComment(portable.id, blocker, input.actor, updatedAt)];
|
|
2014
|
+
portable.updatedAt = updatedAt;
|
|
2015
|
+
await writePortableIssueJson(issuePath, portable);
|
|
2016
|
+
}
|
|
2017
|
+
async function deletePortableIssue(id) {
|
|
2018
|
+
assertSafeIssueId(id);
|
|
2019
|
+
const root = await readableIssueStoreRoot();
|
|
2020
|
+
const record = await readPortableIssue(id, { root, followDuplicate: false });
|
|
2021
|
+
if (!record) return false;
|
|
2022
|
+
await rm(safeJoin(root, "issues", id), { recursive: true, force: true });
|
|
2023
|
+
if (!hasIsolatedIssueStore) await deleteGitBackedPortableIssue(id);
|
|
2024
|
+
return true;
|
|
2025
|
+
}
|
|
2026
|
+
async function deleteGitBackedPortableIssue(id) {
|
|
2027
|
+
try {
|
|
2028
|
+
const { deleteStoredReportBundle } = await import("./report-bundle-sync-3U7QTP4Z.js");
|
|
2029
|
+
await deleteStoredReportBundle(id);
|
|
2030
|
+
} catch (error) {
|
|
2031
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
2032
|
+
throw new Error(`Failed to sync deleted issue ${id} to git-backed issue-store: ${message}`);
|
|
2033
|
+
}
|
|
2034
|
+
}
|
|
2035
|
+
async function portableArtifact(root, issueId, kind, filename, checksum) {
|
|
2036
|
+
assertSafeIssueId(issueId);
|
|
2037
|
+
assertSafeFilename(filename);
|
|
2038
|
+
const relativePath = path2.posix.join("issues", issueId, "artifacts", filename);
|
|
2039
|
+
const fullPath = safeArtifactPath(root, relativePath);
|
|
2040
|
+
const fileStat = await stat(fullPath);
|
|
2041
|
+
const checksumValue = checksum ?? createHash("sha256").update(await readFile2(fullPath)).digest("hex");
|
|
2042
|
+
return {
|
|
2043
|
+
id: `${issueId}-${kind}`,
|
|
2044
|
+
kind,
|
|
2045
|
+
filename,
|
|
2046
|
+
contentType: "text/plain",
|
|
2047
|
+
size: fileStat.size,
|
|
2048
|
+
checksum: checksumValue,
|
|
2049
|
+
relativePath,
|
|
2050
|
+
storage: "issue-store",
|
|
2051
|
+
createdAt: fileStat.birthtime.toISOString()
|
|
2052
|
+
};
|
|
2053
|
+
}
|
|
2054
|
+
function isSafePortableSidecarArtifact(issueId, artifact) {
|
|
2055
|
+
try {
|
|
2056
|
+
if (artifact.kind !== "verification-output" && artifact.kind !== "kernel-suggestion") return false;
|
|
2057
|
+
assertSafeFilename(artifact.filename);
|
|
2058
|
+
if (artifact.storage && artifact.storage !== "issue-store") return false;
|
|
2059
|
+
const expectedPrefix = path2.posix.join("issues", issueId, "artifacts") + "/";
|
|
2060
|
+
if (!artifact.relativePath.startsWith(expectedPrefix)) return false;
|
|
2061
|
+
if (path2.posix.basename(artifact.relativePath) !== artifact.filename) return false;
|
|
2062
|
+
const artifactsRootForIssue = safeJoin(issueStoreRoot, "issues", issueId, "artifacts");
|
|
2063
|
+
const artifactPath = safeArtifactPath(issueStoreRoot, artifact.relativePath);
|
|
2064
|
+
if (!artifactPath.startsWith(`${artifactsRootForIssue}${path2.sep}`)) return false;
|
|
2065
|
+
return true;
|
|
2066
|
+
} catch {
|
|
2067
|
+
return false;
|
|
2068
|
+
}
|
|
2069
|
+
}
|
|
2070
|
+
async function readOptionalFile(filePath) {
|
|
2071
|
+
try {
|
|
2072
|
+
return await readFile2(filePath, "utf8");
|
|
2073
|
+
} catch {
|
|
2074
|
+
return "";
|
|
2075
|
+
}
|
|
2076
|
+
}
|
|
2077
|
+
async function readableIssueStoreRoot() {
|
|
2078
|
+
if (hasExplicitIssueStore || await hasIssuesDirectory(issueStoreRoot)) return issueStoreRoot;
|
|
2079
|
+
return bundledIssueStoreRoot;
|
|
2080
|
+
}
|
|
2081
|
+
async function hasIssuesDirectory(root) {
|
|
2082
|
+
try {
|
|
2083
|
+
const rootStat = await stat(safeJoin(root, "issues"));
|
|
2084
|
+
return rootStat.isDirectory();
|
|
2085
|
+
} catch {
|
|
2086
|
+
return false;
|
|
2087
|
+
}
|
|
2088
|
+
}
|
|
2089
|
+
function existingIssueStoreArtifactPath(relativePath) {
|
|
2090
|
+
const primaryPath = safeArtifactPath(issueStoreRoot, relativePath);
|
|
2091
|
+
if (hasExplicitIssueStore || issueStoreRoot === bundledIssueStoreRoot || existsSync(primaryPath)) return primaryPath;
|
|
2092
|
+
return safeArtifactPath(bundledIssueStoreRoot, relativePath);
|
|
2093
|
+
}
|
|
2094
|
+
|
|
2095
|
+
export {
|
|
2096
|
+
storedGeneratedArtifactFilename,
|
|
2097
|
+
readAutomationSettings,
|
|
2098
|
+
writeAutomationSettings,
|
|
2099
|
+
isAutoLoweringEnabled,
|
|
2100
|
+
listTagDefinitions,
|
|
2101
|
+
addTagDefinition,
|
|
2102
|
+
deleteTagDefinition,
|
|
2103
|
+
createIssue,
|
|
2104
|
+
addComment,
|
|
2105
|
+
updateComment,
|
|
2106
|
+
deleteComment,
|
|
2107
|
+
closeIssue,
|
|
2108
|
+
deleteIssue,
|
|
2109
|
+
updateIssueStatus,
|
|
2110
|
+
updateIssueDescription,
|
|
2111
|
+
addIssueTags,
|
|
2112
|
+
assignIssue,
|
|
2113
|
+
unassignIssue,
|
|
2114
|
+
clearIssueAssignee,
|
|
2115
|
+
addIssueBlocker,
|
|
2116
|
+
syncIssueBlocker,
|
|
2117
|
+
removeIssueBlocker,
|
|
2118
|
+
listIssues,
|
|
2119
|
+
getIssue,
|
|
2120
|
+
readArtifactText,
|
|
2121
|
+
getArtifactTextPath,
|
|
2122
|
+
addKernelSuggestion,
|
|
2123
|
+
updateKernelSuggestion,
|
|
2124
|
+
recordVerificationResult,
|
|
2125
|
+
updateVerificationRunResult,
|
|
2126
|
+
deleteVerificationRuns,
|
|
2127
|
+
hasRunningVerificationRun
|
|
2128
|
+
};
|