@grifhinz/logics-manager 2.6.0 → 2.7.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/VERSION +1 -1
- package/clients/viewer/browser-host.js +409 -10
- package/clients/viewer/index.html +10 -4
- package/clients/viewer/viewer.css +111 -0
- package/logics_manager/assist.py +1 -0
- package/logics_manager/flow.py +6 -2
- package/logics_manager/lint.py +11 -7
- package/logics_manager/viewer.py +359 -14
- package/package.json +1 -1
- package/pyproject.toml +1 -1
package/VERSION
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
2.
|
|
1
|
+
2.7.0
|
|
@@ -13,6 +13,7 @@
|
|
|
13
13
|
const connectionDetail = () => document.getElementById("viewer-connection-detail");
|
|
14
14
|
const filterCount = () => document.getElementById("viewer-filter-count");
|
|
15
15
|
const repoPill = () => document.getElementById("viewer-repo-pill");
|
|
16
|
+
const projectMenu = () => document.getElementById("viewer-project-menu");
|
|
16
17
|
const repoGithubLink = () => document.getElementById("viewer-repo-github");
|
|
17
18
|
const repoFolderButton = () => document.getElementById("viewer-repo-folder");
|
|
18
19
|
const ciButton = () => document.getElementById("viewer-ci");
|
|
@@ -37,6 +38,8 @@
|
|
|
37
38
|
let latestItems = [];
|
|
38
39
|
let latestRepoRoot = "";
|
|
39
40
|
let latestRepository = { root: "", githubUrl: "" };
|
|
41
|
+
let latestCapabilities = {};
|
|
42
|
+
let latestProjects = [];
|
|
40
43
|
let latestMetaText = "Read-only local viewer";
|
|
41
44
|
let autoRefreshIntervalMs = defaultAutoRefreshIntervalMs;
|
|
42
45
|
let nextAutoRefreshAt = 0;
|
|
@@ -273,6 +276,7 @@
|
|
|
273
276
|
|
|
274
277
|
function updateRepositoryIdentity(payload) {
|
|
275
278
|
latestRepoRoot = String(payload.root || latestRepoRoot || "");
|
|
279
|
+
latestProjects = Array.isArray(payload.projects) ? payload.projects : latestProjects;
|
|
276
280
|
const repository = payload.repository && typeof payload.repository === "object" ? payload.repository : {};
|
|
277
281
|
latestRepository = {
|
|
278
282
|
root: String(repository.root || latestRepoRoot || ""),
|
|
@@ -281,10 +285,178 @@
|
|
|
281
285
|
const pill = repoPill();
|
|
282
286
|
if (pill) {
|
|
283
287
|
const repoName = String(payload.repoName || latestRepoRoot.split(/[\\/]/).filter(Boolean).pop() || "repository");
|
|
284
|
-
|
|
288
|
+
const label = pill.querySelector("[data-viewer-project-label]");
|
|
289
|
+
if (label) {
|
|
290
|
+
label.textContent = repoName;
|
|
291
|
+
} else {
|
|
292
|
+
pill.textContent = repoName;
|
|
293
|
+
}
|
|
285
294
|
pill.title = latestRepoRoot || repoName;
|
|
295
|
+
if ("disabled" in pill) {
|
|
296
|
+
pill.disabled = latestProjects.length <= 1;
|
|
297
|
+
}
|
|
298
|
+
pill.onclick = () => {
|
|
299
|
+
const menu = projectMenu();
|
|
300
|
+
setProjectMenuOpen(Boolean(menu?.hidden));
|
|
301
|
+
};
|
|
286
302
|
}
|
|
287
303
|
updateRepositoryShortcuts();
|
|
304
|
+
renderProjectMenu();
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function projectStateLabel(project) {
|
|
308
|
+
if (project?.active) {
|
|
309
|
+
return "current";
|
|
310
|
+
}
|
|
311
|
+
if (project?.available === false) {
|
|
312
|
+
return "missing";
|
|
313
|
+
}
|
|
314
|
+
if (project?.hasLogics === false) {
|
|
315
|
+
return "no Logics";
|
|
316
|
+
}
|
|
317
|
+
return "available";
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
function renderProjectMenu() {
|
|
321
|
+
const menu = projectMenu();
|
|
322
|
+
if (!(menu instanceof HTMLElement)) {
|
|
323
|
+
return;
|
|
324
|
+
}
|
|
325
|
+
const projects = latestProjects.filter((project) => project && typeof project === "object");
|
|
326
|
+
menu.innerHTML = projects.map((project) => `
|
|
327
|
+
<button class="viewer-project-switcher__item${project.active ? " is-active" : ""}" type="button" role="menuitem" data-viewer-project-id="${escapeHtml(project.id || "")}" title="${escapeHtml(project.root || project.name || "")}">
|
|
328
|
+
<span class="viewer-project-switcher__item-name">${escapeHtml(project.name || "project")}</span>
|
|
329
|
+
<span class="viewer-project-switcher__item-state">${escapeHtml(projectStateLabel(project))}</span>
|
|
330
|
+
<span class="viewer-project-switcher__item-path">${escapeHtml(project.root || "")}</span>
|
|
331
|
+
</button>
|
|
332
|
+
`).join("");
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
function setProjectMenuOpen(open) {
|
|
336
|
+
const button = repoPill();
|
|
337
|
+
const menu = projectMenu();
|
|
338
|
+
if (!(button instanceof HTMLElement) || !(menu instanceof HTMLElement)) {
|
|
339
|
+
return;
|
|
340
|
+
}
|
|
341
|
+
const nextOpen = Boolean(open) && latestProjects.length > 1;
|
|
342
|
+
menu.hidden = !nextOpen;
|
|
343
|
+
button.setAttribute("aria-expanded", nextOpen ? "true" : "false");
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
async function switchViewerProject(projectId) {
|
|
347
|
+
if (!projectId) {
|
|
348
|
+
return;
|
|
349
|
+
}
|
|
350
|
+
const target = latestProjects.find((project) => project.id === projectId);
|
|
351
|
+
if (!target || target.active) {
|
|
352
|
+
setProjectMenuOpen(false);
|
|
353
|
+
return;
|
|
354
|
+
}
|
|
355
|
+
setProjectMenuOpen(false);
|
|
356
|
+
setMeta(`Switching to ${target.name || "project"}...`);
|
|
357
|
+
const response = await fetch("/api/switch-project", {
|
|
358
|
+
method: "POST",
|
|
359
|
+
headers: { "Content-Type": "application/json" },
|
|
360
|
+
body: JSON.stringify({ projectId })
|
|
361
|
+
});
|
|
362
|
+
const data = await response.json();
|
|
363
|
+
if (!response.ok || !data.ok) {
|
|
364
|
+
throw new Error(data.error || "Unable to switch project.");
|
|
365
|
+
}
|
|
366
|
+
latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
|
|
367
|
+
latestCiStatus = { visible: false, badgeState: "unknown", message: "" };
|
|
368
|
+
updateMainGitBadges();
|
|
369
|
+
updateMainCiBadge(latestCiStatus);
|
|
370
|
+
updateMainCdxBadge(null);
|
|
371
|
+
const panel = documentPanel();
|
|
372
|
+
if (panel) {
|
|
373
|
+
panel.hidden = true;
|
|
374
|
+
}
|
|
375
|
+
postToApp(data.payload);
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
async function bootstrapLogicsProject() {
|
|
379
|
+
setMeta("Bootstrapping Logics...");
|
|
380
|
+
const response = await fetch("/api/bootstrap-logics", { method: "POST" });
|
|
381
|
+
const data = await response.json();
|
|
382
|
+
if (!response.ok || !data.ok) {
|
|
383
|
+
throw new Error(data.error || "Unable to bootstrap Logics.");
|
|
384
|
+
}
|
|
385
|
+
postToApp(data.payload);
|
|
386
|
+
const created = Array.isArray(data.bootstrap?.created_paths) ? data.bootstrap.created_paths.length : 0;
|
|
387
|
+
setMeta(created > 0 ? `Logics bootstrapped · ${created} paths created.` : "Logics bootstrap checked.");
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
function normalizeCapabilities(payload) {
|
|
391
|
+
const capabilities = payload?.capabilities && typeof payload.capabilities === "object" ? payload.capabilities : {};
|
|
392
|
+
return {
|
|
393
|
+
logics: capabilities.logics || { state: "ready", available: true, message: "" },
|
|
394
|
+
git: capabilities.git || { state: "ready", available: true, message: "" },
|
|
395
|
+
ci: capabilities.ci || { state: "ready", available: true, message: "" },
|
|
396
|
+
cdx: capabilities.cdx || { state: "ready", available: true, message: "" },
|
|
397
|
+
cdxRuns: capabilities.cdxRuns || { state: "unsupported", available: false, message: "" }
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
function capability(name) {
|
|
402
|
+
return latestCapabilities?.[name] || { state: "unknown", available: false, message: "" };
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
function isCapabilityAvailable(name) {
|
|
406
|
+
return capability(name).available === true;
|
|
407
|
+
}
|
|
408
|
+
|
|
409
|
+
function capabilityMessage(name, fallback) {
|
|
410
|
+
return String(capability(name).message || fallback || "");
|
|
411
|
+
}
|
|
412
|
+
|
|
413
|
+
function setButtonUnavailable(button, message) {
|
|
414
|
+
if (!(button instanceof HTMLElement) || !("disabled" in button)) {
|
|
415
|
+
return;
|
|
416
|
+
}
|
|
417
|
+
button.disabled = true;
|
|
418
|
+
button.setAttribute("aria-disabled", "true");
|
|
419
|
+
button.title = message;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
function setButtonAvailable(button, title) {
|
|
423
|
+
if (!(button instanceof HTMLElement) || !("disabled" in button)) {
|
|
424
|
+
return;
|
|
425
|
+
}
|
|
426
|
+
button.disabled = false;
|
|
427
|
+
button.removeAttribute("aria-disabled");
|
|
428
|
+
button.title = title;
|
|
429
|
+
}
|
|
430
|
+
|
|
431
|
+
function updateCapabilityControls() {
|
|
432
|
+
const gitButton = document.getElementById("viewer-git");
|
|
433
|
+
if (gitButton instanceof HTMLElement) {
|
|
434
|
+
gitButton.hidden = !isCapabilityAvailable("git");
|
|
435
|
+
if (isCapabilityAvailable("git")) {
|
|
436
|
+
setButtonAvailable(gitButton, "Show Git status");
|
|
437
|
+
} else {
|
|
438
|
+
setButtonUnavailable(gitButton, capabilityMessage("git", "Git is not available for this project."));
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
|
|
442
|
+
const ci = ciButton();
|
|
443
|
+
if (ci instanceof HTMLElement) {
|
|
444
|
+
ci.hidden = !isCapabilityAvailable("ci");
|
|
445
|
+
if (isCapabilityAvailable("ci")) {
|
|
446
|
+
setButtonAvailable(ci, "Show GitHub Actions CI status");
|
|
447
|
+
} else {
|
|
448
|
+
setButtonUnavailable(ci, capabilityMessage("ci", "CI is not available for this project."));
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
const cdx = document.getElementById("viewer-cdx");
|
|
453
|
+
if (cdx instanceof HTMLElement) {
|
|
454
|
+
if (isCapabilityAvailable("cdx")) {
|
|
455
|
+
setButtonAvailable(cdx, "Show CDX status");
|
|
456
|
+
} else {
|
|
457
|
+
setButtonUnavailable(cdx, capabilityMessage("cdx", "CDX is not available for this project."));
|
|
458
|
+
}
|
|
459
|
+
}
|
|
288
460
|
}
|
|
289
461
|
|
|
290
462
|
function updateRepositoryShortcuts() {
|
|
@@ -430,6 +602,10 @@
|
|
|
430
602
|
}
|
|
431
603
|
|
|
432
604
|
async function refreshCiBadgeCounters() {
|
|
605
|
+
if (!isCapabilityAvailable("ci")) {
|
|
606
|
+
updateMainCiBadge({ visible: false, badgeState: "unknown", message: capabilityMessage("ci", "CI is not available for this project.") });
|
|
607
|
+
return;
|
|
608
|
+
}
|
|
433
609
|
try {
|
|
434
610
|
const response = await fetch("/api/ci-status");
|
|
435
611
|
if (response.status === 404) {
|
|
@@ -476,7 +652,9 @@
|
|
|
476
652
|
button.querySelector("[data-viewer-cdx-badge]")?.remove();
|
|
477
653
|
const activeCount = activeCdxAssistantCountFromPayload(payload);
|
|
478
654
|
if (activeCount <= 0) {
|
|
479
|
-
button.title = "
|
|
655
|
+
button.title = isCapabilityAvailable("cdx")
|
|
656
|
+
? "Show CDX status"
|
|
657
|
+
: capabilityMessage("cdx", "CDX is not available for this project.");
|
|
480
658
|
return;
|
|
481
659
|
}
|
|
482
660
|
const label = activeCount > 9 ? "9+" : String(activeCount);
|
|
@@ -486,6 +664,10 @@
|
|
|
486
664
|
}
|
|
487
665
|
|
|
488
666
|
async function refreshCdxBadgeCounters() {
|
|
667
|
+
if (!isCapabilityAvailable("cdx")) {
|
|
668
|
+
updateMainCdxBadge(null);
|
|
669
|
+
return;
|
|
670
|
+
}
|
|
489
671
|
try {
|
|
490
672
|
const response = await fetch("/api/cdx-status");
|
|
491
673
|
if (response.status === 404) {
|
|
@@ -509,6 +691,11 @@
|
|
|
509
691
|
}
|
|
510
692
|
|
|
511
693
|
async function refreshGitBadgeCounters() {
|
|
694
|
+
if (!isCapabilityAvailable("git")) {
|
|
695
|
+
latestGitBadgeCounts = { unpushedCommits: 0, uncommittedFiles: 0 };
|
|
696
|
+
updateMainGitBadges();
|
|
697
|
+
return;
|
|
698
|
+
}
|
|
512
699
|
try {
|
|
513
700
|
const response = await fetch("/api/git-status");
|
|
514
701
|
const data = await response.json();
|
|
@@ -586,13 +773,10 @@
|
|
|
586
773
|
}, 0);
|
|
587
774
|
}
|
|
588
775
|
|
|
589
|
-
function applyFocusRequest(payload) {
|
|
590
|
-
if (focusApplied) {
|
|
591
|
-
return payload;
|
|
592
|
-
}
|
|
776
|
+
function applyFocusRequest(payload, options = {}) {
|
|
593
777
|
const request = focusRequest();
|
|
594
778
|
if (!request.focus) {
|
|
595
|
-
if (window.location.search.includes("focus=")) {
|
|
779
|
+
if (!focusApplied && !options.silent && window.location.search.includes("focus=")) {
|
|
596
780
|
window.setTimeout(() => setMeta("Invalid focus target. Loaded corpus without changing selection."), 0);
|
|
597
781
|
}
|
|
598
782
|
focusApplied = true;
|
|
@@ -600,14 +784,20 @@
|
|
|
600
784
|
}
|
|
601
785
|
const item = findFocusItem(request.focus);
|
|
602
786
|
if (!item) {
|
|
603
|
-
|
|
787
|
+
if (!focusApplied && !options.silent) {
|
|
788
|
+
window.setTimeout(() => setMeta(`Focus target not found: ${request.focus}`), 0);
|
|
789
|
+
}
|
|
604
790
|
focusApplied = true;
|
|
605
791
|
return payload;
|
|
606
792
|
}
|
|
793
|
+
const nextPayload = { ...payload, selectedId: item.id };
|
|
794
|
+
if (focusApplied) {
|
|
795
|
+
persistSelectedItem(item.id);
|
|
796
|
+
return nextPayload;
|
|
797
|
+
}
|
|
607
798
|
viewerFilterState = { ...viewerFilterState, focus: "all", type: "all", status: "any", relation: "any", activity: "any" };
|
|
608
799
|
persistSelectedItem(item.id);
|
|
609
800
|
focusApplied = true;
|
|
610
|
-
const nextPayload = { ...payload, selectedId: item.id };
|
|
611
801
|
window.setTimeout(() => {
|
|
612
802
|
revealFocusedCard(item);
|
|
613
803
|
if (request.read) {
|
|
@@ -765,8 +955,10 @@
|
|
|
765
955
|
updateRefreshIntervalControl();
|
|
766
956
|
}
|
|
767
957
|
updateRepositoryIdentity(payload);
|
|
958
|
+
latestCapabilities = normalizeCapabilities(payload);
|
|
959
|
+
updateCapabilityControls();
|
|
768
960
|
const payloadWithActivity = { ...payload, items: latestItems };
|
|
769
|
-
const nextPayload =
|
|
961
|
+
const nextPayload = applyFocusRequest(payloadWithActivity, { silent: Boolean(options.silent) });
|
|
770
962
|
window.dispatchEvent(new MessageEvent("message", { data: { type: "data", payload: nextPayload } }));
|
|
771
963
|
const rootName = payload.root ? payload.root.split(/[\\/]/).filter(Boolean).pop() : "repository";
|
|
772
964
|
if (!options.silent) {
|
|
@@ -840,6 +1032,12 @@
|
|
|
840
1032
|
return Boolean(panel && !panel.hidden && title && title.textContent === "CDX status");
|
|
841
1033
|
}
|
|
842
1034
|
|
|
1035
|
+
function isCdxRunsOpen() {
|
|
1036
|
+
const panel = documentPanel();
|
|
1037
|
+
const title = documentTitle();
|
|
1038
|
+
return Boolean(panel && !panel.hidden && title && title.textContent === "CDX runs");
|
|
1039
|
+
}
|
|
1040
|
+
|
|
843
1041
|
function isCiStatusOpen() {
|
|
844
1042
|
const panel = documentPanel();
|
|
845
1043
|
const title = documentTitle();
|
|
@@ -854,6 +1052,8 @@
|
|
|
854
1052
|
await showCiStatus({ silent: Boolean(options.silent) });
|
|
855
1053
|
} else if (isCdxStatusOpen()) {
|
|
856
1054
|
await showCdxStatus({ silent: Boolean(options.silent) });
|
|
1055
|
+
} else if (isCdxRunsOpen()) {
|
|
1056
|
+
await showCdxRuns({ silent: Boolean(options.silent) });
|
|
857
1057
|
} else if (method === "POST") {
|
|
858
1058
|
await refreshGitBadgeCounters();
|
|
859
1059
|
}
|
|
@@ -1906,10 +2106,20 @@
|
|
|
1906
2106
|
return rows || `<li class="viewer-cdx__empty">${escapeHtml(emptyText)}</li>`;
|
|
1907
2107
|
}
|
|
1908
2108
|
|
|
2109
|
+
function renderCdxModeSwitcher(active) {
|
|
2110
|
+
return `
|
|
2111
|
+
<div class="viewer-cdx__modes" role="tablist" aria-label="CDX views">
|
|
2112
|
+
<button class="viewer-cdx__mode${active === "status" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="status" aria-selected="${active === "status" ? "true" : "false"}">Status</button>
|
|
2113
|
+
<button class="viewer-cdx__mode${active === "runs" ? " is-active" : ""}" type="button" data-viewer-cdx-mode="runs" aria-selected="${active === "runs" ? "true" : "false"}">Runs</button>
|
|
2114
|
+
</div>
|
|
2115
|
+
`;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
1909
2118
|
function renderCdxStatus(payload) {
|
|
1910
2119
|
if (!payload || payload.state !== "ok") {
|
|
1911
2120
|
return `
|
|
1912
2121
|
<div class="viewer-cdx">
|
|
2122
|
+
${renderCdxModeSwitcher("status")}
|
|
1913
2123
|
<div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX status is unavailable.")}</div>
|
|
1914
2124
|
</div>
|
|
1915
2125
|
`;
|
|
@@ -1945,6 +2155,7 @@
|
|
|
1945
2155
|
`).join("");
|
|
1946
2156
|
return `
|
|
1947
2157
|
<div class="viewer-cdx">
|
|
2158
|
+
${renderCdxModeSwitcher("status")}
|
|
1948
2159
|
<div class="viewer-cdx__summary">${cards}</div>
|
|
1949
2160
|
<div class="viewer-cdx__workspace">
|
|
1950
2161
|
<div class="viewer-cdx__stack">
|
|
@@ -1972,7 +2183,87 @@
|
|
|
1972
2183
|
`;
|
|
1973
2184
|
}
|
|
1974
2185
|
|
|
2186
|
+
function renderCdxRuns(payload) {
|
|
2187
|
+
if (!payload || payload.state !== "ok") {
|
|
2188
|
+
return `
|
|
2189
|
+
<div class="viewer-cdx">
|
|
2190
|
+
${renderCdxModeSwitcher("runs")}
|
|
2191
|
+
<div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX runs are unavailable.")}</div>
|
|
2192
|
+
</div>
|
|
2193
|
+
`;
|
|
2194
|
+
}
|
|
2195
|
+
const runs = Array.isArray(payload.runs) ? payload.runs : [];
|
|
2196
|
+
const rows = runs.map((run) => `
|
|
2197
|
+
<tr>
|
|
2198
|
+
<td><code>${escapeHtml(run.run_id || "-")}</code></td>
|
|
2199
|
+
<td>${renderCdxBadge(run.status || "unknown")}</td>
|
|
2200
|
+
<td>${escapeHtml(run.kind || "assistant")}</td>
|
|
2201
|
+
<td>${escapeHtml(run.session || "-")}</td>
|
|
2202
|
+
<td>${escapeHtml(run.cwd || "-")}</td>
|
|
2203
|
+
<td><button class="viewer-cdx__mode" type="button" data-viewer-cdx-report="${escapeHtml(run.run_id || "")}">Report</button></td>
|
|
2204
|
+
</tr>
|
|
2205
|
+
`).join("");
|
|
2206
|
+
return `
|
|
2207
|
+
<div class="viewer-cdx">
|
|
2208
|
+
${renderCdxModeSwitcher("runs")}
|
|
2209
|
+
<section class="viewer-cdx__section">
|
|
2210
|
+
<div class="viewer-ci__heading"><h2>Assistant runs</h2><span>${escapeHtml(runs.length)} reported</span></div>
|
|
2211
|
+
<div class="viewer-cdx__table-wrap">
|
|
2212
|
+
<table class="viewer-cdx__table">
|
|
2213
|
+
<thead><tr><th>RUN</th><th>STATUS</th><th>KIND</th><th>SESSION</th><th>CWD</th><th>REPORT</th></tr></thead>
|
|
2214
|
+
<tbody>${rows || '<tr><td colspan="6" class="viewer-cdx__empty">No assistant runs reported.</td></tr>'}</tbody>
|
|
2215
|
+
</table>
|
|
2216
|
+
</div>
|
|
2217
|
+
</section>
|
|
2218
|
+
</div>
|
|
2219
|
+
`;
|
|
2220
|
+
}
|
|
2221
|
+
|
|
2222
|
+
function renderCdxReport(payload) {
|
|
2223
|
+
if (!payload || payload.state !== "ok" || !payload.report) {
|
|
2224
|
+
return `
|
|
2225
|
+
<div class="viewer-cdx">
|
|
2226
|
+
${renderCdxModeSwitcher("runs")}
|
|
2227
|
+
<div class="viewer-cdx__state">${escapeHtml(payload?.message || "CDX run report is unavailable.")}</div>
|
|
2228
|
+
</div>
|
|
2229
|
+
`;
|
|
2230
|
+
}
|
|
2231
|
+
const report = payload.report || {};
|
|
2232
|
+
const run = report.run || {};
|
|
2233
|
+
const taskReport = report.task_report || {};
|
|
2234
|
+
const findings = Array.isArray(taskReport.findings) ? taskReport.findings : [];
|
|
2235
|
+
const findingRows = findings.map((finding, index) => {
|
|
2236
|
+
const location = [finding.path || finding.file || "", finding.line || ""].filter(Boolean).join(":") || "-";
|
|
2237
|
+
return `<li class="viewer-cdx__entity"><div class="viewer-cdx__entity-main"><div><strong>${escapeHtml(finding.message || finding.title || `Finding ${index + 1}`)}</strong><div class="viewer-cdx__meta">${escapeHtml(location)}</div></div>${renderCdxBadge(finding.severity || "unknown")}</div></li>`;
|
|
2238
|
+
}).join("");
|
|
2239
|
+
const canCreate = taskReport.kind === "code-review";
|
|
2240
|
+
return `
|
|
2241
|
+
<div class="viewer-cdx">
|
|
2242
|
+
${renderCdxModeSwitcher("runs")}
|
|
2243
|
+
<section class="viewer-cdx__section">
|
|
2244
|
+
<div class="viewer-ci__heading"><h2>Run report</h2><span>${escapeHtml(run.status || "unknown")}</span></div>
|
|
2245
|
+
<ul class="viewer-cdx__list">
|
|
2246
|
+
<li class="viewer-cdx__row"><span>Run</span><strong>${escapeHtml(run.run_id || taskReport.run_id || "-")}</strong></li>
|
|
2247
|
+
<li class="viewer-cdx__row"><span>Kind</span><strong>${escapeHtml(taskReport.kind || run.kind || "assistant")}</strong></li>
|
|
2248
|
+
<li class="viewer-cdx__row"><span>Summary</span><strong>${escapeHtml(taskReport.summary || "No summary reported.")}</strong></li>
|
|
2249
|
+
</ul>
|
|
2250
|
+
${canCreate ? `<button class="btn" type="button" data-viewer-cdx-create-request="${escapeHtml(run.run_id || taskReport.run_id || "")}">Create Logics request</button>` : ""}
|
|
2251
|
+
</section>
|
|
2252
|
+
<section class="viewer-cdx__section">
|
|
2253
|
+
<div class="viewer-ci__heading"><h2>Findings</h2><span>${escapeHtml(findings.length)} reported</span></div>
|
|
2254
|
+
<ul class="viewer-cdx__list">${findingRows || '<li class="viewer-cdx__empty">No structured findings reported.</li>'}</ul>
|
|
2255
|
+
</section>
|
|
2256
|
+
</div>
|
|
2257
|
+
`;
|
|
2258
|
+
}
|
|
2259
|
+
|
|
1975
2260
|
async function showCdxStatus(options = {}) {
|
|
2261
|
+
if (!isCapabilityAvailable("cdx")) {
|
|
2262
|
+
const message = capabilityMessage("cdx", "CDX is not available for this project.");
|
|
2263
|
+
setDocument("CDX status", renderCdxStatus({ state: capability("cdx").state, message }));
|
|
2264
|
+
setMeta(message);
|
|
2265
|
+
return;
|
|
2266
|
+
}
|
|
1976
2267
|
if (!options.silent) {
|
|
1977
2268
|
setMeta("Checking CDX status...");
|
|
1978
2269
|
}
|
|
@@ -1999,6 +2290,62 @@
|
|
|
1999
2290
|
setMeta(options.silent ? "CDX status refreshed." : "CDX status loaded.");
|
|
2000
2291
|
}
|
|
2001
2292
|
|
|
2293
|
+
async function showCdxRuns(options = {}) {
|
|
2294
|
+
if (!isCapabilityAvailable("cdx")) {
|
|
2295
|
+
const message = capabilityMessage("cdx", "CDX is not available for this project.");
|
|
2296
|
+
setDocument("CDX runs", renderCdxRuns({ state: capability("cdx").state, message }));
|
|
2297
|
+
setMeta(message);
|
|
2298
|
+
return;
|
|
2299
|
+
}
|
|
2300
|
+
if (!options.silent) {
|
|
2301
|
+
setMeta("Checking CDX runs...");
|
|
2302
|
+
}
|
|
2303
|
+
const response = await fetch("/api/cdx-runs");
|
|
2304
|
+
let data = {};
|
|
2305
|
+
try {
|
|
2306
|
+
data = await response.json();
|
|
2307
|
+
} catch {
|
|
2308
|
+
data = {};
|
|
2309
|
+
}
|
|
2310
|
+
if (!response.ok || !data.ok) {
|
|
2311
|
+
throw new Error(data.error || "Unable to load CDX runs.");
|
|
2312
|
+
}
|
|
2313
|
+
setDocument("CDX runs", renderCdxRuns(data.payload));
|
|
2314
|
+
setMeta(options.silent ? "CDX runs refreshed." : "CDX runs loaded.");
|
|
2315
|
+
}
|
|
2316
|
+
|
|
2317
|
+
async function showCdxReport(runId) {
|
|
2318
|
+
if (!runId) {
|
|
2319
|
+
return;
|
|
2320
|
+
}
|
|
2321
|
+
setMeta("Loading CDX report...");
|
|
2322
|
+
const response = await fetch(`/api/cdx-run-report?${new URLSearchParams({ runId }).toString()}`);
|
|
2323
|
+
const data = await response.json();
|
|
2324
|
+
if (!response.ok || !data.ok) {
|
|
2325
|
+
throw new Error(data.error || "Unable to load CDX report.");
|
|
2326
|
+
}
|
|
2327
|
+
setDocument("CDX run report", renderCdxReport(data.payload));
|
|
2328
|
+
setMeta("CDX report loaded.");
|
|
2329
|
+
}
|
|
2330
|
+
|
|
2331
|
+
async function createRequestFromCdxReport(runId) {
|
|
2332
|
+
if (!runId) {
|
|
2333
|
+
return;
|
|
2334
|
+
}
|
|
2335
|
+
setMeta("Creating Logics request from CDX report...");
|
|
2336
|
+
const response = await fetch("/api/cdx-report-request", {
|
|
2337
|
+
method: "POST",
|
|
2338
|
+
headers: { "Content-Type": "application/json" },
|
|
2339
|
+
body: JSON.stringify({ runId })
|
|
2340
|
+
});
|
|
2341
|
+
const data = await response.json();
|
|
2342
|
+
if (!response.ok || !data.ok) {
|
|
2343
|
+
throw new Error(data.error || "Unable to create Logics request.");
|
|
2344
|
+
}
|
|
2345
|
+
postToApp(data.payload);
|
|
2346
|
+
setMeta(`Created ${data.created?.id || "Logics request"} from CDX report.`);
|
|
2347
|
+
}
|
|
2348
|
+
|
|
2002
2349
|
function renderCiBadge(value) {
|
|
2003
2350
|
const tone = ciBadgeTone(value);
|
|
2004
2351
|
return `<span class="viewer-ci__badge viewer-ci__badge--${escapeHtml(tone)}">${escapeHtml(ciBadgeLabel(value))}</span>`;
|
|
@@ -2068,6 +2415,12 @@
|
|
|
2068
2415
|
}
|
|
2069
2416
|
|
|
2070
2417
|
async function showCiStatus(options = {}) {
|
|
2418
|
+
if (!isCapabilityAvailable("ci")) {
|
|
2419
|
+
const message = capabilityMessage("ci", "CI is not available for this project.");
|
|
2420
|
+
setDocument("CI status", renderCiStatus({ visible: false, state: capability("ci").state, message }));
|
|
2421
|
+
setMeta(message);
|
|
2422
|
+
return;
|
|
2423
|
+
}
|
|
2071
2424
|
if (!options.silent) {
|
|
2072
2425
|
setMeta("Checking CI status...");
|
|
2073
2426
|
}
|
|
@@ -2341,6 +2694,12 @@
|
|
|
2341
2694
|
|
|
2342
2695
|
async function showGitStatus(options = {}) {
|
|
2343
2696
|
const previous = options.preserve ? currentGitViewState() : { domain: "changes", path: "", cached: false };
|
|
2697
|
+
if (!isCapabilityAvailable("git")) {
|
|
2698
|
+
const message = capabilityMessage("git", "Git is not available for this project.");
|
|
2699
|
+
setDocument("Git status", renderGitStatus({ state: capability("git").state, message }));
|
|
2700
|
+
setMeta(message);
|
|
2701
|
+
return;
|
|
2702
|
+
}
|
|
2344
2703
|
if (!options.silent) {
|
|
2345
2704
|
setMeta("Checking Git status...");
|
|
2346
2705
|
}
|
|
@@ -2388,6 +2747,10 @@
|
|
|
2388
2747
|
refreshViewer("POST").catch((error) => setMeta(error.message));
|
|
2389
2748
|
return;
|
|
2390
2749
|
}
|
|
2750
|
+
if (message.type === "bootstrap-logics") {
|
|
2751
|
+
bootstrapLogicsProject().catch((error) => setMeta(error.message));
|
|
2752
|
+
return;
|
|
2753
|
+
}
|
|
2391
2754
|
if (message.type === "open" || message.type === "read") {
|
|
2392
2755
|
const item = latestItems.find((entry) => entry.id === message.id);
|
|
2393
2756
|
showDocument(item).catch((error) => setMeta(error.message));
|
|
@@ -2480,6 +2843,10 @@
|
|
|
2480
2843
|
document.getElementById("viewer-cdx")?.addEventListener("click", () => {
|
|
2481
2844
|
showCdxStatus().catch((error) => setMeta(error.message));
|
|
2482
2845
|
});
|
|
2846
|
+
repoPill()?.addEventListener("click", () => {
|
|
2847
|
+
const menu = projectMenu();
|
|
2848
|
+
setProjectMenuOpen(Boolean(menu?.hidden));
|
|
2849
|
+
});
|
|
2483
2850
|
repoFolderButton()?.addEventListener("click", () => {
|
|
2484
2851
|
openRepositoryFolder().catch((error) => setMeta(error.message));
|
|
2485
2852
|
});
|
|
@@ -2518,6 +2885,38 @@
|
|
|
2518
2885
|
const gitHistoryRevealTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-history-reveal]") : null;
|
|
2519
2886
|
const gitDomainTarget = event.target instanceof Element ? event.target.closest(".viewer-git__domain[data-viewer-git-domain]") : null;
|
|
2520
2887
|
const gitFileTarget = event.target instanceof Element ? event.target.closest("[data-viewer-git-file]") : null;
|
|
2888
|
+
const projectSwitcherTarget = event.target instanceof Element ? event.target.closest("#viewer-repo-pill") : null;
|
|
2889
|
+
const projectTarget = event.target instanceof Element ? event.target.closest("[data-viewer-project-id]") : null;
|
|
2890
|
+
const cdxModeTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-mode]") : null;
|
|
2891
|
+
const cdxReportTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-report]") : null;
|
|
2892
|
+
const cdxCreateRequestTarget = event.target instanceof Element ? event.target.closest("[data-viewer-cdx-create-request]") : null;
|
|
2893
|
+
if (cdxReportTarget instanceof HTMLElement) {
|
|
2894
|
+
showCdxReport(cdxReportTarget.getAttribute("data-viewer-cdx-report") || "").catch((error) => setMeta(error.message));
|
|
2895
|
+
return;
|
|
2896
|
+
}
|
|
2897
|
+
if (cdxCreateRequestTarget instanceof HTMLElement) {
|
|
2898
|
+
createRequestFromCdxReport(cdxCreateRequestTarget.getAttribute("data-viewer-cdx-create-request") || "").catch((error) => setMeta(error.message));
|
|
2899
|
+
return;
|
|
2900
|
+
}
|
|
2901
|
+
if (cdxModeTarget instanceof HTMLElement) {
|
|
2902
|
+
const mode = cdxModeTarget.getAttribute("data-viewer-cdx-mode") || "status";
|
|
2903
|
+
if (mode === "runs") {
|
|
2904
|
+
showCdxRuns().catch((error) => setMeta(error.message));
|
|
2905
|
+
} else {
|
|
2906
|
+
showCdxStatus().catch((error) => setMeta(error.message));
|
|
2907
|
+
}
|
|
2908
|
+
return;
|
|
2909
|
+
}
|
|
2910
|
+
if (projectSwitcherTarget instanceof HTMLElement) {
|
|
2911
|
+
const menu = projectMenu();
|
|
2912
|
+
setProjectMenuOpen(Boolean(menu?.hidden));
|
|
2913
|
+
return;
|
|
2914
|
+
}
|
|
2915
|
+
if (projectTarget instanceof HTMLElement) {
|
|
2916
|
+
event.preventDefault();
|
|
2917
|
+
switchViewerProject(projectTarget.getAttribute("data-viewer-project-id") || "").catch((error) => setMeta(error.message));
|
|
2918
|
+
return;
|
|
2919
|
+
}
|
|
2521
2920
|
if (gitHistoryRevealTarget instanceof HTMLElement) {
|
|
2522
2921
|
event.preventDefault();
|
|
2523
2922
|
event.stopImmediatePropagation();
|
|
@@ -18,7 +18,13 @@
|
|
|
18
18
|
<div>
|
|
19
19
|
<div class="viewer-topbar__identity">
|
|
20
20
|
<div class="viewer-topbar__title">Logics Viewer</div>
|
|
21
|
-
<
|
|
21
|
+
<div class="viewer-project-switcher" id="viewer-project-switcher">
|
|
22
|
+
<button class="viewer-topbar__repo viewer-project-switcher__button" id="viewer-repo-pill" type="button" title="" aria-haspopup="menu" aria-expanded="false" aria-controls="viewer-project-menu">
|
|
23
|
+
<span data-viewer-project-label>repository</span>
|
|
24
|
+
<span class="viewer-project-switcher__chevron" aria-hidden="true">v</span>
|
|
25
|
+
</button>
|
|
26
|
+
<div class="viewer-project-switcher__menu" id="viewer-project-menu" role="menu" aria-label="Known projects" hidden></div>
|
|
27
|
+
</div>
|
|
22
28
|
<span class="viewer-topbar__repo-actions" id="viewer-repo-actions" aria-label="Repository shortcuts">
|
|
23
29
|
<a class="viewer-topbar__repo-action" id="viewer-repo-github" href="#" target="_blank" rel="noreferrer" title="Open GitHub repository" aria-label="Open GitHub repository" hidden>
|
|
24
30
|
<svg viewBox="0 0 24 24" aria-hidden="true" focusable="false"><path d="M9 19c-4.2 1.2-4.2-2-6-2.4m12 4.4v-3.5c0-1 .1-1.4-.5-2 2-.2 4.1-1 4.1-4.6 0-1-.4-1.9-1-2.6.1-.3.4-1.3-.1-2.6 0 0-.8-.3-2.7 1a9.3 9.3 0 0 0-4.9 0c-1.8-1.3-2.7-1-2.7-1-.5 1.3-.2 2.3-.1 2.6a3.8 3.8 0 0 0-1 2.6c0 3.6 2.1 4.4 4.1 4.6-.5.5-.7 1-.7 1.9V21" fill="none" stroke="currentColor" stroke-width="1.8" stroke-linecap="round" stroke-linejoin="round" /></svg>
|
|
@@ -31,6 +37,9 @@
|
|
|
31
37
|
<div class="viewer-topbar__meta" id="viewer-meta">Read-only local viewer</div>
|
|
32
38
|
</div>
|
|
33
39
|
<div class="viewer-topbar__actions">
|
|
40
|
+
<button class="btn" id="viewer-git" type="button" title="Show Git status">Git</button>
|
|
41
|
+
<button class="btn" id="viewer-ci" type="button" title="Show GitHub Actions CI status" hidden>CI</button>
|
|
42
|
+
<button class="btn" id="viewer-cdx" type="button" title="Show CDX status">CDX</button>
|
|
34
43
|
<div class="viewer-refresh-menu">
|
|
35
44
|
<button class="btn" id="viewer-refresh-menu-button" type="button" title="Viewer settings" aria-haspopup="menu" aria-expanded="false" aria-controls="viewer-refresh-menu">Settings</button>
|
|
36
45
|
<div class="viewer-refresh-menu__panel" id="viewer-refresh-menu" role="menu" aria-label="Viewer settings" hidden>
|
|
@@ -59,9 +68,6 @@
|
|
|
59
68
|
</section>
|
|
60
69
|
</div>
|
|
61
70
|
</div>
|
|
62
|
-
<button class="btn" id="viewer-git" type="button" title="Show Git status">Git</button>
|
|
63
|
-
<button class="btn" id="viewer-ci" type="button" title="Show GitHub Actions CI status" hidden>CI</button>
|
|
64
|
-
<button class="btn" id="viewer-cdx" type="button" title="Show CDX status">CDX</button>
|
|
65
71
|
</div>
|
|
66
72
|
</header>
|
|
67
73
|
|
|
@@ -37,6 +37,92 @@
|
|
|
37
37
|
line-height: 1.3;
|
|
38
38
|
}
|
|
39
39
|
|
|
40
|
+
.viewer-project-switcher {
|
|
41
|
+
position: relative;
|
|
42
|
+
min-width: 0;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
.viewer-project-switcher__button {
|
|
46
|
+
display: inline-flex;
|
|
47
|
+
align-items: center;
|
|
48
|
+
gap: 6px;
|
|
49
|
+
cursor: pointer;
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
.viewer-project-switcher__button[disabled] {
|
|
53
|
+
cursor: default;
|
|
54
|
+
opacity: 0.75;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
.viewer-project-switcher__chevron {
|
|
58
|
+
font-size: 9px;
|
|
59
|
+
opacity: 0.75;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
.viewer-project-switcher__menu {
|
|
63
|
+
position: absolute;
|
|
64
|
+
z-index: 20;
|
|
65
|
+
top: calc(100% + 6px);
|
|
66
|
+
left: 0;
|
|
67
|
+
min-width: 260px;
|
|
68
|
+
max-width: min(420px, calc(100vw - 24px));
|
|
69
|
+
max-height: min(360px, calc(100vh - 96px));
|
|
70
|
+
overflow: auto;
|
|
71
|
+
padding: 6px;
|
|
72
|
+
border: 1px solid var(--vscode-panel-border, #333333);
|
|
73
|
+
border-radius: 6px;
|
|
74
|
+
background: var(--vscode-dropdown-background, var(--vscode-editorWidget-background, #202020));
|
|
75
|
+
box-shadow: 0 12px 28px rgb(0 0 0 / 28%);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
.viewer-project-switcher__menu[hidden] {
|
|
79
|
+
display: none !important;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
.viewer-project-switcher__item {
|
|
83
|
+
width: 100%;
|
|
84
|
+
display: grid;
|
|
85
|
+
grid-template-columns: minmax(0, 1fr) auto;
|
|
86
|
+
gap: 4px 10px;
|
|
87
|
+
padding: 7px 8px;
|
|
88
|
+
border: 0;
|
|
89
|
+
border-radius: 4px;
|
|
90
|
+
background: transparent;
|
|
91
|
+
color: var(--vscode-foreground, #d4d4d4);
|
|
92
|
+
text-align: left;
|
|
93
|
+
cursor: pointer;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.viewer-project-switcher__item:hover,
|
|
97
|
+
.viewer-project-switcher__item.is-active {
|
|
98
|
+
background: var(--vscode-list-hoverBackground, rgb(255 255 255 / 8%));
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
.viewer-project-switcher__item-name,
|
|
102
|
+
.viewer-project-switcher__item-state {
|
|
103
|
+
overflow: hidden;
|
|
104
|
+
text-overflow: ellipsis;
|
|
105
|
+
white-space: nowrap;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
.viewer-project-switcher__item-name {
|
|
109
|
+
font-weight: 650;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
.viewer-project-switcher__item-path {
|
|
113
|
+
grid-column: 1 / -1;
|
|
114
|
+
overflow: hidden;
|
|
115
|
+
text-overflow: ellipsis;
|
|
116
|
+
white-space: nowrap;
|
|
117
|
+
color: var(--vscode-descriptionForeground, #9da5b4);
|
|
118
|
+
font-size: 11px;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
.viewer-project-switcher__item-state {
|
|
122
|
+
color: var(--vscode-descriptionForeground, #9da5b4);
|
|
123
|
+
font-size: 11px;
|
|
124
|
+
}
|
|
125
|
+
|
|
40
126
|
.viewer-topbar__repo-actions {
|
|
41
127
|
display: inline-flex;
|
|
42
128
|
align-items: center;
|
|
@@ -529,6 +615,31 @@
|
|
|
529
615
|
gap: 14px;
|
|
530
616
|
}
|
|
531
617
|
|
|
618
|
+
.viewer-cdx__modes {
|
|
619
|
+
display: inline-flex;
|
|
620
|
+
align-items: center;
|
|
621
|
+
gap: 2px;
|
|
622
|
+
width: fit-content;
|
|
623
|
+
padding: 2px;
|
|
624
|
+
border: 1px solid var(--vscode-panel-border, #333333);
|
|
625
|
+
border-radius: 6px;
|
|
626
|
+
background: var(--vscode-editorWidget-background, #202020);
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
.viewer-cdx__mode {
|
|
630
|
+
border: 0;
|
|
631
|
+
border-radius: 4px;
|
|
632
|
+
padding: 5px 10px;
|
|
633
|
+
background: transparent;
|
|
634
|
+
color: var(--vscode-descriptionForeground, #9da5b4);
|
|
635
|
+
cursor: pointer;
|
|
636
|
+
}
|
|
637
|
+
|
|
638
|
+
.viewer-cdx__mode.is-active {
|
|
639
|
+
background: var(--vscode-button-secondaryBackground, #3a3d41);
|
|
640
|
+
color: var(--vscode-button-secondaryForeground, var(--vscode-foreground, #d4d4d4));
|
|
641
|
+
}
|
|
642
|
+
|
|
532
643
|
.viewer-git__summary,
|
|
533
644
|
.viewer-cdx__summary,
|
|
534
645
|
.viewer-ci__summary,
|
package/logics_manager/assist.py
CHANGED
|
@@ -336,6 +336,7 @@ def _build_spec_first_pass(repo_root: Path, backlog_ref: str) -> dict[str, objec
|
|
|
336
336
|
[
|
|
337
337
|
f"## {ref} - {spec_title}",
|
|
338
338
|
f"> From version: {_parse_package_version(repo_root)}",
|
|
339
|
+
"> Status: Draft",
|
|
339
340
|
"> Understanding: 90%",
|
|
340
341
|
"> Confidence: 85%",
|
|
341
342
|
"",
|
package/logics_manager/flow.py
CHANGED
|
@@ -800,6 +800,8 @@ def _workflow_mermaid_block(kind: str, signature: str) -> list[str]:
|
|
|
800
800
|
|
|
801
801
|
|
|
802
802
|
def _with_workflow_mermaid_overview(kind: str, content: str) -> str:
|
|
803
|
+
if kind == "task":
|
|
804
|
+
return content
|
|
803
805
|
lines = content.rstrip().splitlines()
|
|
804
806
|
signature = expected_workflow_mermaid_signature(kind, lines)
|
|
805
807
|
if not signature:
|
|
@@ -995,6 +997,8 @@ def _mermaid_closeout_issue(path: Path, kind: str) -> str | None:
|
|
|
995
997
|
text = path.read_text(encoding="utf-8")
|
|
996
998
|
match = re.search(r"```mermaid\s*\n(.*?)\n```", text, flags=re.DOTALL)
|
|
997
999
|
if match is None:
|
|
1000
|
+
if kind == "task":
|
|
1001
|
+
return None
|
|
998
1002
|
return "missing Mermaid overview block"
|
|
999
1003
|
signature_match = re.search(r"^\s*%%\s*logics-signature:\s*(.+?)\s*$", match.group(1), flags=re.MULTILINE)
|
|
1000
1004
|
expected = expected_workflow_mermaid_signature(kind, text.splitlines())
|
|
@@ -1625,7 +1629,7 @@ def _build_native_task_doc(
|
|
|
1625
1629
|
"",
|
|
1626
1630
|
]
|
|
1627
1631
|
).rstrip() + "\n"
|
|
1628
|
-
return
|
|
1632
|
+
return content
|
|
1629
1633
|
|
|
1630
1634
|
|
|
1631
1635
|
def _extract_doc_title(path: Path) -> str:
|
|
@@ -2183,7 +2187,7 @@ def _build_native_task_from_backlog(
|
|
|
2183
2187
|
"",
|
|
2184
2188
|
]
|
|
2185
2189
|
).rstrip() + "\n"
|
|
2186
|
-
return ref,
|
|
2190
|
+
return ref, content
|
|
2187
2191
|
|
|
2188
2192
|
|
|
2189
2193
|
def build_parser() -> argparse.ArgumentParser:
|
package/logics_manager/lint.py
CHANGED
|
@@ -15,18 +15,19 @@ from .config import find_repo_root
|
|
|
15
15
|
@dataclass(frozen=True)
|
|
16
16
|
class Kind:
|
|
17
17
|
directory: str
|
|
18
|
-
|
|
18
|
+
prefixes: tuple[str, ...]
|
|
19
19
|
requires_progress: bool
|
|
20
20
|
required_indicators: tuple[str, ...]
|
|
21
21
|
allowed_statuses: tuple[str, ...]
|
|
22
22
|
|
|
23
23
|
|
|
24
24
|
KINDS = {
|
|
25
|
-
"request": Kind("logics/request", "req", False, ("From version", "Understanding", "Confidence"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
|
|
26
|
-
"backlog": Kind("logics/backlog", "item", True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
|
|
27
|
-
"task": Kind("logics/tasks", "task", True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
|
|
28
|
-
"product": Kind("logics/product", "prod", False, ("Date", "Status", "Related request", "Related backlog", "Related task", "Related architecture", "Reminder"), ("Draft", "Proposed", "Active", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
|
|
29
|
-
"architecture": Kind("logics/architecture", "adr", False, ("Date", "Status", "Drivers", "Related request", "Related backlog", "Related task", "Reminder"), ("Draft", "Proposed", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
|
|
25
|
+
"request": Kind("logics/request", ("req",), False, ("From version", "Understanding", "Confidence"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
|
|
26
|
+
"backlog": Kind("logics/backlog", ("item",), True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
|
|
27
|
+
"task": Kind("logics/tasks", ("task",), True, ("From version", "Understanding", "Confidence", "Progress"), ("Draft", "Ready", "In progress", "Blocked", "Done", "Obsolete", "Archived")),
|
|
28
|
+
"product": Kind("logics/product", ("prod",), False, ("Date", "Status", "Related request", "Related backlog", "Related task", "Related architecture", "Reminder"), ("Draft", "Proposed", "Active", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
|
|
29
|
+
"architecture": Kind("logics/architecture", ("adr",), False, ("Date", "Status", "Drivers", "Related request", "Related backlog", "Related task", "Reminder"), ("Draft", "Proposed", "Accepted", "Validated", "Rejected", "Superseded", "Settled", "Archived")),
|
|
30
|
+
"spec": Kind("logics/specs", ("spec", "req"), False, ("From version", "Status", "Understanding", "Confidence"), ("Draft", "Ready", "In progress", "Done", "Validated", "Settled", "Archived")),
|
|
30
31
|
}
|
|
31
32
|
|
|
32
33
|
WORKFLOW_KINDS = {"request", "backlog", "task"}
|
|
@@ -503,7 +504,8 @@ def _lint_file(path: Path, kind_name: str, kind: Kind, require_status: bool, che
|
|
|
503
504
|
issues: list[str] = []
|
|
504
505
|
warnings: list[str] = []
|
|
505
506
|
name = path.name
|
|
506
|
-
|
|
507
|
+
allowed_prefixes = "|".join(re.escape(prefix) for prefix in kind.prefixes)
|
|
508
|
+
if not re.match(rf"^({allowed_prefixes})_\d{{3}}_[a-z0-9_]+\.md$", name):
|
|
507
509
|
issues.append(f"bad filename: {name}")
|
|
508
510
|
|
|
509
511
|
lines = _read_lines(path)
|
|
@@ -557,6 +559,8 @@ def lint_payload(repo_root: Path, *, require_status: bool = False) -> dict[str,
|
|
|
557
559
|
if not directory.is_dir():
|
|
558
560
|
continue
|
|
559
561
|
for path in sorted(directory.glob("*.md")):
|
|
562
|
+
if path.name == "README.md":
|
|
563
|
+
continue
|
|
560
564
|
rel_path = path.relative_to(repo_root)
|
|
561
565
|
issues, warnings = _lint_file(
|
|
562
566
|
path,
|
package/logics_manager/viewer.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
from __future__ import annotations
|
|
2
2
|
|
|
3
3
|
import argparse
|
|
4
|
+
import hashlib
|
|
4
5
|
import json
|
|
5
6
|
import mimetypes
|
|
6
7
|
import os
|
|
@@ -20,6 +21,7 @@ from typing import Any
|
|
|
20
21
|
from urllib.parse import parse_qs, quote, unquote, urlencode, urlparse
|
|
21
22
|
|
|
22
23
|
from .audit import audit_payload
|
|
24
|
+
from .bootstrap import bootstrap_payload
|
|
23
25
|
from .config import find_repo_root
|
|
24
26
|
from .lint import lint_payload
|
|
25
27
|
from .update_check import get_update_info
|
|
@@ -342,22 +344,28 @@ def viewer_data_payload(
|
|
|
342
344
|
selected_id: str | None = None,
|
|
343
345
|
*,
|
|
344
346
|
auto_refresh_interval_seconds: int = 15,
|
|
347
|
+
projects: list[dict[str, Any]] | None = None,
|
|
345
348
|
) -> dict[str, Any]:
|
|
349
|
+
capabilities = viewer_project_capabilities(repo_root)
|
|
350
|
+
active_root = repo_root.resolve()
|
|
351
|
+
has_logics = capabilities["logics"]["available"] is True
|
|
346
352
|
return {
|
|
347
|
-
"root": str(
|
|
348
|
-
"repoName":
|
|
353
|
+
"root": str(active_root),
|
|
354
|
+
"repoName": active_root.name,
|
|
349
355
|
"repository": {
|
|
350
|
-
"root": str(
|
|
356
|
+
"root": str(active_root),
|
|
351
357
|
"githubUrl": github_repo_url(repo_root),
|
|
352
358
|
},
|
|
359
|
+
"capabilities": capabilities,
|
|
360
|
+
"projects": projects if projects is not None else viewer_project_registry(repo_root),
|
|
353
361
|
"autoRefreshIntervalSeconds": auto_refresh_interval_seconds,
|
|
354
362
|
"items": collect_viewer_items(repo_root),
|
|
355
363
|
"updateInfo": get_update_info(_current_version()).to_payload(),
|
|
356
364
|
"selectedId": selected_id,
|
|
357
365
|
"changedPaths": [],
|
|
358
366
|
"canResetProjectRoot": False,
|
|
359
|
-
"canBootstrapLogics":
|
|
360
|
-
"bootstrapLogicsTitle": "
|
|
367
|
+
"canBootstrapLogics": not has_logics,
|
|
368
|
+
"bootstrapLogicsTitle": "Bootstrap Logics in this project." if not has_logics else "Logics is already bootstrapped.",
|
|
361
369
|
"canLaunchCodex": False,
|
|
362
370
|
"canLaunchClaude": False,
|
|
363
371
|
"canRepairLogicsKit": False,
|
|
@@ -366,6 +374,155 @@ def viewer_data_payload(
|
|
|
366
374
|
}
|
|
367
375
|
|
|
368
376
|
|
|
377
|
+
def _viewer_project_id(repo_root: Path) -> str:
|
|
378
|
+
normalized = str(repo_root.resolve())
|
|
379
|
+
return hashlib.sha1(normalized.encode("utf-8")).hexdigest()[:12]
|
|
380
|
+
|
|
381
|
+
|
|
382
|
+
def _looks_like_viewer_project(path: Path) -> bool:
|
|
383
|
+
if not path.is_dir():
|
|
384
|
+
return False
|
|
385
|
+
return any((path / marker).exists() for marker in ("logics", ".git", "package.json", "pyproject.toml", "logics.yaml"))
|
|
386
|
+
|
|
387
|
+
|
|
388
|
+
def discover_viewer_project_roots(repo_root: Path, *, max_projects: int = 40) -> list[Path]:
|
|
389
|
+
active = repo_root.resolve()
|
|
390
|
+
candidates: list[Path] = [active]
|
|
391
|
+
parent = active.parent
|
|
392
|
+
try:
|
|
393
|
+
siblings = sorted(parent.iterdir(), key=lambda path: path.name.lower())
|
|
394
|
+
except OSError:
|
|
395
|
+
siblings = []
|
|
396
|
+
for sibling in siblings:
|
|
397
|
+
try:
|
|
398
|
+
resolved = sibling.resolve()
|
|
399
|
+
except OSError:
|
|
400
|
+
continue
|
|
401
|
+
if resolved == active or not _looks_like_viewer_project(resolved):
|
|
402
|
+
continue
|
|
403
|
+
candidates.append(resolved)
|
|
404
|
+
if len(candidates) >= max_projects:
|
|
405
|
+
break
|
|
406
|
+
|
|
407
|
+
unique: dict[str, Path] = {}
|
|
408
|
+
for candidate in candidates:
|
|
409
|
+
unique[str(candidate)] = candidate
|
|
410
|
+
return list(unique.values())
|
|
411
|
+
|
|
412
|
+
|
|
413
|
+
def viewer_project_entry(repo_root: Path, *, active_root: Path | None = None) -> dict[str, Any]:
|
|
414
|
+
root = repo_root.resolve()
|
|
415
|
+
active = active_root.resolve() if active_root else root
|
|
416
|
+
has_logics = (root / "logics").is_dir()
|
|
417
|
+
available = root.is_dir()
|
|
418
|
+
return {
|
|
419
|
+
"id": _viewer_project_id(root),
|
|
420
|
+
"name": root.name,
|
|
421
|
+
"root": str(root),
|
|
422
|
+
"active": root == active,
|
|
423
|
+
"available": available,
|
|
424
|
+
"hasLogics": has_logics,
|
|
425
|
+
"message": "Logics corpus found." if has_logics else "No Logics corpus found.",
|
|
426
|
+
}
|
|
427
|
+
|
|
428
|
+
|
|
429
|
+
def viewer_project_registry(repo_root: Path, *, project_roots: list[Path] | None = None) -> list[dict[str, Any]]:
|
|
430
|
+
active = repo_root.resolve()
|
|
431
|
+
roots = project_roots if project_roots is not None else discover_viewer_project_roots(active)
|
|
432
|
+
return [viewer_project_entry(root, active_root=active) for root in roots]
|
|
433
|
+
|
|
434
|
+
|
|
435
|
+
def _viewer_capability(state: str, *, available: bool, message: str, detail: dict[str, Any] | None = None) -> dict[str, Any]:
|
|
436
|
+
payload: dict[str, Any] = {
|
|
437
|
+
"state": state,
|
|
438
|
+
"available": available,
|
|
439
|
+
"message": message,
|
|
440
|
+
}
|
|
441
|
+
if detail:
|
|
442
|
+
payload["detail"] = detail
|
|
443
|
+
return payload
|
|
444
|
+
|
|
445
|
+
|
|
446
|
+
def _git_is_repository(repo_root: Path, *, runner: Any | None = None) -> bool | None:
|
|
447
|
+
try:
|
|
448
|
+
result = _run_read_only_git(repo_root, ["rev-parse", "--is-inside-work-tree"], runner=runner)
|
|
449
|
+
except (OSError, subprocess.SubprocessError):
|
|
450
|
+
return None
|
|
451
|
+
if result.returncode != 0:
|
|
452
|
+
return False
|
|
453
|
+
return result.stdout.strip().lower() == "true"
|
|
454
|
+
|
|
455
|
+
|
|
456
|
+
def viewer_project_capabilities(
|
|
457
|
+
repo_root: Path,
|
|
458
|
+
*,
|
|
459
|
+
git_runner: Any | None = None,
|
|
460
|
+
which: Any | None = None,
|
|
461
|
+
) -> dict[str, Any]:
|
|
462
|
+
which_command = which or shutil.which
|
|
463
|
+
logics_dir = repo_root / "logics"
|
|
464
|
+
has_logics = logics_dir.is_dir()
|
|
465
|
+
git_path = which_command("git")
|
|
466
|
+
cdx_path = which_command("cdx")
|
|
467
|
+
|
|
468
|
+
if has_logics:
|
|
469
|
+
logics = _viewer_capability("ready", available=True, message="Logics corpus found.")
|
|
470
|
+
else:
|
|
471
|
+
logics = _viewer_capability("missing", available=False, message="No Logics corpus found.")
|
|
472
|
+
|
|
473
|
+
if not git_path:
|
|
474
|
+
git = _viewer_capability("unavailable", available=False, message="Git executable is not available.")
|
|
475
|
+
github_url = ""
|
|
476
|
+
has_workflows = False
|
|
477
|
+
else:
|
|
478
|
+
is_repo = _git_is_repository(repo_root, runner=git_runner)
|
|
479
|
+
if is_repo is True:
|
|
480
|
+
git = _viewer_capability("ready", available=True, message="Git repository detected.")
|
|
481
|
+
github_url = github_repo_url(repo_root, runner=git_runner, which=which_command)
|
|
482
|
+
has_workflows = _has_github_actions_workflows(repo_root)
|
|
483
|
+
elif is_repo is False:
|
|
484
|
+
git = _viewer_capability("missing", available=False, message="Project is not a Git repository.")
|
|
485
|
+
github_url = ""
|
|
486
|
+
has_workflows = False
|
|
487
|
+
else:
|
|
488
|
+
git = _viewer_capability("error", available=False, message="Unable to inspect Git repository state.")
|
|
489
|
+
github_url = ""
|
|
490
|
+
has_workflows = False
|
|
491
|
+
|
|
492
|
+
if not github_url:
|
|
493
|
+
ci = _viewer_capability("hidden", available=False, message="No GitHub remote detected for this project.")
|
|
494
|
+
elif not has_workflows:
|
|
495
|
+
ci = _viewer_capability("hidden", available=False, message="No GitHub Actions workflows detected for this project.")
|
|
496
|
+
elif not which_command("gh"):
|
|
497
|
+
ci = _viewer_capability("unavailable", available=False, message="GitHub CLI is not available.")
|
|
498
|
+
else:
|
|
499
|
+
ci = _viewer_capability(
|
|
500
|
+
"ready",
|
|
501
|
+
available=True,
|
|
502
|
+
message="GitHub Actions can be inspected.",
|
|
503
|
+
detail={"githubUrl": github_url},
|
|
504
|
+
)
|
|
505
|
+
|
|
506
|
+
if cdx_path:
|
|
507
|
+
cdx = _viewer_capability("ready", available=True, message="CDX executable detected.")
|
|
508
|
+
cdx_runs = _viewer_capability(
|
|
509
|
+
"unsupported",
|
|
510
|
+
available=False,
|
|
511
|
+
message="CDX assistant run registry is not available yet.",
|
|
512
|
+
)
|
|
513
|
+
else:
|
|
514
|
+
cdx = _viewer_capability("missing", available=False, message="CDX executable is not available.")
|
|
515
|
+
cdx_runs = _viewer_capability("missing", available=False, message="CDX is required before assistant runs can be tracked.")
|
|
516
|
+
|
|
517
|
+
return {
|
|
518
|
+
"logics": logics,
|
|
519
|
+
"git": git,
|
|
520
|
+
"ci": ci,
|
|
521
|
+
"cdx": cdx,
|
|
522
|
+
"cdxRuns": cdx_runs,
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
|
|
369
526
|
def read_doc_payload(repo_root: Path, rel_path: str) -> dict[str, Any]:
|
|
370
527
|
normalized, absolute = _resolve_repo_doc_path(repo_root, rel_path)
|
|
371
528
|
return {
|
|
@@ -939,6 +1096,123 @@ def cdx_status_payload(repo_root: Path, *, runner: Any | None = None, which: Any
|
|
|
939
1096
|
return {"state": "ok", "message": "", "status": parsed}
|
|
940
1097
|
|
|
941
1098
|
|
|
1099
|
+
def cdx_runs_payload(repo_root: Path, *, runner: Any | None = None, which: Any | None = None) -> dict[str, Any]:
|
|
1100
|
+
cdx_which = which or shutil.which
|
|
1101
|
+
if not cdx_which("cdx"):
|
|
1102
|
+
return {"state": "unavailable", "message": "CDX executable is not available on PATH.", "runs": []}
|
|
1103
|
+
try:
|
|
1104
|
+
result = _run_read_only_cdx(repo_root, ["runs", "--json"], runner=runner)
|
|
1105
|
+
except subprocess.TimeoutExpired:
|
|
1106
|
+
return {"state": "timeout", "message": "CDX runs timed out.", "runs": []}
|
|
1107
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
1108
|
+
return {"state": "error", "message": f"Unable to run CDX runs: {exc}", "runs": []}
|
|
1109
|
+
if result.returncode != 0:
|
|
1110
|
+
message = (result.stderr or result.stdout or "CDX runs failed.").strip().splitlines()[0]
|
|
1111
|
+
return {"state": "error", "message": message, "runs": []}
|
|
1112
|
+
try:
|
|
1113
|
+
parsed = json.loads(result.stdout or "{}")
|
|
1114
|
+
except json.JSONDecodeError:
|
|
1115
|
+
return {"state": "invalid-json", "message": "CDX runs returned invalid JSON.", "runs": []}
|
|
1116
|
+
runs = parsed.get("runs") if isinstance(parsed, dict) else None
|
|
1117
|
+
if not isinstance(runs, list):
|
|
1118
|
+
return {"state": "invalid-json", "message": "CDX runs JSON must include a runs array.", "runs": []}
|
|
1119
|
+
return {"state": "ok", "message": "", "runs": [run for run in runs if isinstance(run, dict)]}
|
|
1120
|
+
|
|
1121
|
+
|
|
1122
|
+
def cdx_run_report_payload(repo_root: Path, run_id: str, *, runner: Any | None = None, which: Any | None = None) -> dict[str, Any]:
|
|
1123
|
+
cdx_which = which or shutil.which
|
|
1124
|
+
if not run_id:
|
|
1125
|
+
return {"state": "error", "message": "Missing CDX run id.", "report": None}
|
|
1126
|
+
if not cdx_which("cdx"):
|
|
1127
|
+
return {"state": "unavailable", "message": "CDX executable is not available on PATH.", "report": None}
|
|
1128
|
+
try:
|
|
1129
|
+
result = _run_read_only_cdx(repo_root, ["run-report", run_id, "--json"], runner=runner)
|
|
1130
|
+
except subprocess.TimeoutExpired:
|
|
1131
|
+
return {"state": "timeout", "message": "CDX run report timed out.", "report": None}
|
|
1132
|
+
except (OSError, subprocess.SubprocessError) as exc:
|
|
1133
|
+
return {"state": "error", "message": f"Unable to run CDX run-report: {exc}", "report": None}
|
|
1134
|
+
if result.returncode != 0:
|
|
1135
|
+
message = (result.stderr or result.stdout or "CDX run-report failed.").strip().splitlines()[0]
|
|
1136
|
+
return {"state": "error", "message": message, "report": None}
|
|
1137
|
+
try:
|
|
1138
|
+
parsed = json.loads(result.stdout or "{}")
|
|
1139
|
+
except json.JSONDecodeError:
|
|
1140
|
+
return {"state": "invalid-json", "message": "CDX run-report returned invalid JSON.", "report": None}
|
|
1141
|
+
report = parsed.get("report") if isinstance(parsed, dict) else None
|
|
1142
|
+
if not isinstance(report, dict):
|
|
1143
|
+
return {"state": "invalid-json", "message": "CDX run-report JSON must include a report object.", "report": None}
|
|
1144
|
+
return {"state": "ok", "message": "", "report": report}
|
|
1145
|
+
|
|
1146
|
+
|
|
1147
|
+
def _slugify_viewer_doc(text: str) -> str:
|
|
1148
|
+
slug = re.sub(r"[^a-z0-9]+", "_", text.lower()).strip("_")
|
|
1149
|
+
return slug[:80] or "cdx_code_review_findings"
|
|
1150
|
+
|
|
1151
|
+
|
|
1152
|
+
def _next_viewer_request_ref(repo_root: Path, title: str) -> str:
|
|
1153
|
+
request_dir = repo_root / "logics" / "request"
|
|
1154
|
+
highest = -1
|
|
1155
|
+
if request_dir.is_dir():
|
|
1156
|
+
for path in request_dir.glob("req_*.md"):
|
|
1157
|
+
match = re.match(r"^req_(\d{3})_", path.stem)
|
|
1158
|
+
if match:
|
|
1159
|
+
highest = max(highest, int(match.group(1)))
|
|
1160
|
+
return f"req_{highest + 1:03d}_{_slugify_viewer_doc(title)}"
|
|
1161
|
+
|
|
1162
|
+
|
|
1163
|
+
def create_request_from_cdx_report(repo_root: Path, report_payload: dict[str, Any]) -> dict[str, Any]:
|
|
1164
|
+
report = report_payload.get("report") if isinstance(report_payload.get("report"), dict) else report_payload
|
|
1165
|
+
run = report.get("run") if isinstance(report.get("run"), dict) else {}
|
|
1166
|
+
task_report = report.get("task_report") if isinstance(report.get("task_report"), dict) else {}
|
|
1167
|
+
run_id = str(run.get("run_id") or task_report.get("run_id") or "unknown")
|
|
1168
|
+
findings = task_report.get("findings") if isinstance(task_report.get("findings"), list) else []
|
|
1169
|
+
title = f"Address CDX code review findings for {run_id}"
|
|
1170
|
+
ref = _next_viewer_request_ref(repo_root, title)
|
|
1171
|
+
request_dir = repo_root / "logics" / "request"
|
|
1172
|
+
request_dir.mkdir(parents=True, exist_ok=True)
|
|
1173
|
+
rel_path = f"logics/request/{ref}.md"
|
|
1174
|
+
path = repo_root / rel_path
|
|
1175
|
+
finding_lines = []
|
|
1176
|
+
for index, finding in enumerate(findings, start=1):
|
|
1177
|
+
if not isinstance(finding, dict):
|
|
1178
|
+
continue
|
|
1179
|
+
location = finding.get("path") or finding.get("file") or "unknown path"
|
|
1180
|
+
if finding.get("line"):
|
|
1181
|
+
location = f"{location}:{finding['line']}"
|
|
1182
|
+
severity = finding.get("severity") or "unknown"
|
|
1183
|
+
message = finding.get("message") or finding.get("title") or "Review finding"
|
|
1184
|
+
finding_lines.append(f"- F{index} [{severity}] `{location}`: {message}")
|
|
1185
|
+
if not finding_lines:
|
|
1186
|
+
finding_lines.append("- No structured findings were reported. Review the CDX artifacts linked below.")
|
|
1187
|
+
text = "\n".join([
|
|
1188
|
+
f"## {ref} - {title}",
|
|
1189
|
+
"> Status: Draft",
|
|
1190
|
+
"> Understanding: 70%",
|
|
1191
|
+
"> Confidence: 70%",
|
|
1192
|
+
"> Complexity: Medium",
|
|
1193
|
+
"> Theme: Code review follow-up",
|
|
1194
|
+
"",
|
|
1195
|
+
"# Needs",
|
|
1196
|
+
f"- Follow up on CDX code-review run `{run_id}`.",
|
|
1197
|
+
f"- Summary: {task_report.get('summary') or 'No structured summary provided.'}",
|
|
1198
|
+
"",
|
|
1199
|
+
"# Findings",
|
|
1200
|
+
*finding_lines,
|
|
1201
|
+
"",
|
|
1202
|
+
"# Traceability",
|
|
1203
|
+
f"- CDX run id: `{run_id}`",
|
|
1204
|
+
f"- Transcript: `{(report.get('artifacts') or {}).get('transcript_path') or ''}`",
|
|
1205
|
+
f"- Stdout: `{(report.get('artifacts') or {}).get('stdout_path') or ''}`",
|
|
1206
|
+
"",
|
|
1207
|
+
"# Acceptance Criteria",
|
|
1208
|
+
"- AC1: Each actionable finding is reviewed and either fixed, documented as not applicable, or split into follow-up work.",
|
|
1209
|
+
"- AC2: Validation evidence is added before closing this request.",
|
|
1210
|
+
"",
|
|
1211
|
+
])
|
|
1212
|
+
path.write_text(text, encoding="utf-8")
|
|
1213
|
+
return {"id": ref, "path": rel_path, "title": title}
|
|
1214
|
+
|
|
1215
|
+
|
|
942
1216
|
def _json_bytes(payload: Any) -> bytes:
|
|
943
1217
|
return json.dumps(payload, indent=2, sort_keys=True).encode("utf-8")
|
|
944
1218
|
|
|
@@ -951,10 +1225,35 @@ class LogicsViewerServer(ThreadingHTTPServer):
|
|
|
951
1225
|
*,
|
|
952
1226
|
auto_refresh_interval_seconds: int = 15,
|
|
953
1227
|
):
|
|
954
|
-
self.
|
|
1228
|
+
self.launch_repo_root = repo_root.resolve()
|
|
1229
|
+
self.project_roots = discover_viewer_project_roots(self.launch_repo_root)
|
|
1230
|
+
self.project_root_by_id = {_viewer_project_id(root): root.resolve() for root in self.project_roots}
|
|
1231
|
+
self.active_project_id = _viewer_project_id(self.launch_repo_root)
|
|
1232
|
+
self.repo_root = self.launch_repo_root
|
|
955
1233
|
self.auto_refresh_interval_seconds = auto_refresh_interval_seconds
|
|
956
1234
|
super().__init__(server_address, LogicsViewerRequestHandler)
|
|
957
1235
|
|
|
1236
|
+
def project_registry_payload(self) -> list[dict[str, Any]]:
|
|
1237
|
+
return viewer_project_registry(self.repo_root, project_roots=self.project_roots)
|
|
1238
|
+
|
|
1239
|
+
def viewer_payload(self, *, selected_id: str | None = None) -> dict[str, Any]:
|
|
1240
|
+
return viewer_data_payload(
|
|
1241
|
+
self.repo_root,
|
|
1242
|
+
selected_id=selected_id,
|
|
1243
|
+
auto_refresh_interval_seconds=self.auto_refresh_interval_seconds,
|
|
1244
|
+
projects=self.project_registry_payload(),
|
|
1245
|
+
)
|
|
1246
|
+
|
|
1247
|
+
def switch_project(self, project_id: str) -> dict[str, Any]:
|
|
1248
|
+
target = self.project_root_by_id.get(project_id)
|
|
1249
|
+
if target is None:
|
|
1250
|
+
raise ValueError("Unknown project id.")
|
|
1251
|
+
if not target.is_dir():
|
|
1252
|
+
raise FileNotFoundError(str(target))
|
|
1253
|
+
self.active_project_id = project_id
|
|
1254
|
+
self.repo_root = target
|
|
1255
|
+
return self.viewer_payload()
|
|
1256
|
+
|
|
958
1257
|
|
|
959
1258
|
class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
960
1259
|
server: LogicsViewerServer
|
|
@@ -1018,13 +1317,13 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
1018
1317
|
self._send_json(
|
|
1019
1318
|
{
|
|
1020
1319
|
"ok": True,
|
|
1021
|
-
"payload":
|
|
1022
|
-
self.server.repo_root,
|
|
1023
|
-
auto_refresh_interval_seconds=self.server.auto_refresh_interval_seconds,
|
|
1024
|
-
),
|
|
1320
|
+
"payload": self.server.viewer_payload(),
|
|
1025
1321
|
}
|
|
1026
1322
|
)
|
|
1027
1323
|
return
|
|
1324
|
+
if route == "/api/projects":
|
|
1325
|
+
self._send_json({"ok": True, "payload": {"projects": self.server.project_registry_payload()}})
|
|
1326
|
+
return
|
|
1028
1327
|
if route == "/api/doc":
|
|
1029
1328
|
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
1030
1329
|
try:
|
|
@@ -1038,6 +1337,9 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
1038
1337
|
if route == "/api/audit":
|
|
1039
1338
|
self._send_json({"ok": True, "payload": audit_payload(self.server.repo_root)})
|
|
1040
1339
|
return
|
|
1340
|
+
if route == "/api/capabilities":
|
|
1341
|
+
self._send_json({"ok": True, "payload": viewer_project_capabilities(self.server.repo_root)})
|
|
1342
|
+
return
|
|
1041
1343
|
if route == "/api/git-status":
|
|
1042
1344
|
self._send_json({"ok": True, "payload": git_status_payload(self.server.repo_root)})
|
|
1043
1345
|
return
|
|
@@ -1047,6 +1349,13 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
1047
1349
|
if route == "/api/cdx-status":
|
|
1048
1350
|
self._send_json({"ok": True, "payload": cdx_status_payload(self.server.repo_root)})
|
|
1049
1351
|
return
|
|
1352
|
+
if route == "/api/cdx-runs":
|
|
1353
|
+
self._send_json({"ok": True, "payload": cdx_runs_payload(self.server.repo_root)})
|
|
1354
|
+
return
|
|
1355
|
+
if route == "/api/cdx-run-report":
|
|
1356
|
+
run_id = parse_qs(parsed.query).get("runId", [""])[0]
|
|
1357
|
+
self._send_json({"ok": True, "payload": cdx_run_report_payload(self.server.repo_root, run_id)})
|
|
1358
|
+
return
|
|
1050
1359
|
if route == "/api/git-diff":
|
|
1051
1360
|
params = parse_qs(parsed.query)
|
|
1052
1361
|
rel_path = params.get("path", [""])[0]
|
|
@@ -1061,13 +1370,49 @@ class LogicsViewerRequestHandler(BaseHTTPRequestHandler):
|
|
|
1061
1370
|
self._send_json(
|
|
1062
1371
|
{
|
|
1063
1372
|
"ok": True,
|
|
1064
|
-
"payload":
|
|
1065
|
-
self.server.repo_root,
|
|
1066
|
-
auto_refresh_interval_seconds=self.server.auto_refresh_interval_seconds,
|
|
1067
|
-
),
|
|
1373
|
+
"payload": self.server.viewer_payload(),
|
|
1068
1374
|
}
|
|
1069
1375
|
)
|
|
1070
1376
|
return
|
|
1377
|
+
if parsed.path == "/api/switch-project":
|
|
1378
|
+
try:
|
|
1379
|
+
length = int(self.headers.get("Content-Length", "0") or "0")
|
|
1380
|
+
raw_body = self.rfile.read(length).decode("utf-8") if length > 0 else "{}"
|
|
1381
|
+
body = json.loads(raw_body or "{}")
|
|
1382
|
+
project_id = str(body.get("projectId") or "")
|
|
1383
|
+
self._send_json({"ok": True, "payload": self.server.switch_project(project_id)})
|
|
1384
|
+
except json.JSONDecodeError:
|
|
1385
|
+
self._send_error_json(HTTPStatus.BAD_REQUEST, "Invalid JSON body.")
|
|
1386
|
+
except ValueError as exc:
|
|
1387
|
+
self._send_error_json(HTTPStatus.FORBIDDEN, str(exc))
|
|
1388
|
+
except FileNotFoundError as exc:
|
|
1389
|
+
self._send_error_json(HTTPStatus.NOT_FOUND, str(exc))
|
|
1390
|
+
return
|
|
1391
|
+
if parsed.path == "/api/bootstrap-logics":
|
|
1392
|
+
try:
|
|
1393
|
+
bootstrap = bootstrap_payload(self.server.repo_root, check=False)
|
|
1394
|
+
self._send_json({"ok": True, "payload": self.server.viewer_payload(), "bootstrap": bootstrap})
|
|
1395
|
+
except SystemExit as exc:
|
|
1396
|
+
self._send_error_json(HTTPStatus.BAD_REQUEST, str(exc))
|
|
1397
|
+
except OSError as exc:
|
|
1398
|
+
self._send_error_json(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
|
|
1399
|
+
return
|
|
1400
|
+
if parsed.path == "/api/cdx-report-request":
|
|
1401
|
+
try:
|
|
1402
|
+
length = int(self.headers.get("Content-Length", "0") or "0")
|
|
1403
|
+
raw_body = self.rfile.read(length).decode("utf-8") if length > 0 else "{}"
|
|
1404
|
+
body = json.loads(raw_body or "{}")
|
|
1405
|
+
report_payload = cdx_run_report_payload(self.server.repo_root, str(body.get("runId") or ""))
|
|
1406
|
+
if report_payload.get("state") != "ok":
|
|
1407
|
+
self._send_error_json(HTTPStatus.BAD_GATEWAY, str(report_payload.get("message") or "Unable to load CDX report."))
|
|
1408
|
+
return
|
|
1409
|
+
created = create_request_from_cdx_report(self.server.repo_root, report_payload)
|
|
1410
|
+
self._send_json({"ok": True, "created": created, "payload": self.server.viewer_payload(selected_id=created["id"])})
|
|
1411
|
+
except json.JSONDecodeError:
|
|
1412
|
+
self._send_error_json(HTTPStatus.BAD_REQUEST, "Invalid JSON body.")
|
|
1413
|
+
except OSError as exc:
|
|
1414
|
+
self._send_error_json(HTTPStatus.INTERNAL_SERVER_ERROR, str(exc))
|
|
1415
|
+
return
|
|
1071
1416
|
if parsed.path == "/api/edit":
|
|
1072
1417
|
rel_path = parse_qs(parsed.query).get("path", [""])[0]
|
|
1073
1418
|
try:
|
package/package.json
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
"name": "@grifhinz/logics-manager",
|
|
3
3
|
"displayName": "Logics Orchestrator",
|
|
4
4
|
"description": "Visual orchestration for Logics workflows inside VS Code.",
|
|
5
|
-
"version": "2.
|
|
5
|
+
"version": "2.7.0",
|
|
6
6
|
"publisher": "cdx-logics",
|
|
7
7
|
"icon": "clients/shared-web/media/icon.png",
|
|
8
8
|
"repository": {
|