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.
Files changed (52) hide show
  1. package/README.md +138 -0
  2. package/dist/help.txt +70 -0
  3. package/dist/lagora.js +342 -0
  4. package/dist/report-help.txt +5 -0
  5. package/dist/scripts/agora_playground_harness.py +263 -0
  6. package/dist/scripts/announce.js +41 -0
  7. package/dist/scripts/check-kernel-submission.py +90 -0
  8. package/dist/scripts/chunk-2EAJVB5D.js +100 -0
  9. package/dist/scripts/chunk-2KTLCUFI.js +29 -0
  10. package/dist/scripts/chunk-AZ3EEBVD.js +137 -0
  11. package/dist/scripts/chunk-NBJMYAOA.js +2128 -0
  12. package/dist/scripts/chunk-NCJMUBTG.js +125 -0
  13. package/dist/scripts/chunk-QJPQHKIO.js +23 -0
  14. package/dist/scripts/chunk-RIR5KGHC.js +33 -0
  15. package/dist/scripts/chunk-TJZVQYBL.js +8 -0
  16. package/dist/scripts/chunk-UHJXD4TG.js +18 -0
  17. package/dist/scripts/chunk-UQ6I6VTY.js +117 -0
  18. package/dist/scripts/cli-auth.js +348 -0
  19. package/dist/scripts/cli-config-IA7EOSYD.js +7 -0
  20. package/dist/scripts/install-skill.js +199 -0
  21. package/dist/scripts/issue-local-client-DZUXZOKY.js +22 -0
  22. package/dist/scripts/issue-search.js +1823 -0
  23. package/dist/scripts/issue.js +386 -0
  24. package/dist/scripts/keycloak-provision.js +986 -0
  25. package/dist/scripts/legato-fsim-runner.py +126 -0
  26. package/dist/scripts/legato-lowering-runner.py +156 -0
  27. package/dist/scripts/legato_runner_annotations.py +235 -0
  28. package/dist/scripts/legato_runner_env.py +91 -0
  29. package/dist/scripts/legato_runner_launchers.py +287 -0
  30. package/dist/scripts/legato_runner_script_wrapper.py +193 -0
  31. package/dist/scripts/notifications-EU43SIEV.js +624 -0
  32. package/dist/scripts/playground.js +408 -0
  33. package/dist/scripts/report-bundle-sync-3U7QTP4Z.js +215 -0
  34. package/dist/scripts/report.js +104 -0
  35. package/dist/scripts/resolve-sdk-package-version.py +151 -0
  36. package/dist/scripts/sdk-runtime-JE6H2PB2.js +992 -0
  37. package/dist/scripts/sdk-runtime-kubernetes-job-KOWL4ITV.js +479 -0
  38. package/dist/scripts/sdk-runtime-smoke.py +168 -0
  39. package/dist/scripts/sdk.js +256 -0
  40. package/dist/scripts/site-feedback-CAPE5MPX.js +136 -0
  41. package/dist/scripts/site-feedback-rate-limit-5BU2WSFE.js +86 -0
  42. package/dist/scripts/site-feedback.js +117 -0
  43. package/dist/scripts/storage-234FBH54.js +67 -0
  44. package/dist/scripts/submit-issue.sh +489 -0
  45. package/dist/scripts/verification-3QCY66QW.js +772 -0
  46. package/dist/scripts/verify-issue.js +144 -0
  47. package/dist/skills/legato-agora-cli/SKILL.md +556 -0
  48. package/dist/skills/legato-agora-cli/agents/openai.yaml +7 -0
  49. package/dist/skills/legato-agora-cli/reference/kernel-with-golden.py +84 -0
  50. package/dist/skills/legato-site-feedback/SKILL.md +49 -0
  51. package/dist/skills/legato-site-feedback/agents/openai.yaml +7 -0
  52. 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
+ };