@grifhinz/logics-manager 2.2.0 → 2.3.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 (43) hide show
  1. package/README.md +60 -1
  2. package/VERSION +1 -1
  3. package/clients/README.md +9 -0
  4. package/clients/shared-web/media/css/board.css +658 -0
  5. package/clients/shared-web/media/css/details.css +457 -0
  6. package/clients/shared-web/media/css/layout.css +123 -0
  7. package/clients/shared-web/media/css/toolbar.css +576 -0
  8. package/clients/shared-web/media/harnessApi.js +324 -0
  9. package/clients/shared-web/media/hostApi.js +213 -0
  10. package/clients/shared-web/media/hostApiContract.js +55 -0
  11. package/clients/shared-web/media/icon.png +0 -0
  12. package/clients/shared-web/media/layoutController.js +246 -0
  13. package/clients/shared-web/media/logics.svg +7 -0
  14. package/clients/shared-web/media/logicsModel.js +910 -0
  15. package/clients/shared-web/media/main.css +112 -0
  16. package/clients/shared-web/media/main.js +3 -0
  17. package/clients/shared-web/media/mainApp.js +1005 -0
  18. package/clients/shared-web/media/mainCore.js +604 -0
  19. package/clients/shared-web/media/mainInteractionHandlers.js +324 -0
  20. package/clients/shared-web/media/mainInteractions.js +378 -0
  21. package/clients/shared-web/media/renderBoard.js +3 -0
  22. package/clients/shared-web/media/renderBoardApp.js +1339 -0
  23. package/clients/shared-web/media/renderDetails.js +685 -0
  24. package/clients/shared-web/media/renderMarkdown.js +449 -0
  25. package/clients/shared-web/media/toolsPanelLayout.js +172 -0
  26. package/clients/shared-web/media/uiStatus.js +54 -0
  27. package/clients/shared-web/media/webviewChrome.js +405 -0
  28. package/clients/shared-web/media/webviewPersistence.js +116 -0
  29. package/clients/shared-web/media/webviewSelectors.js +491 -0
  30. package/clients/viewer/README.md +5 -0
  31. package/clients/viewer/browser-host.js +847 -0
  32. package/clients/viewer/index.html +237 -0
  33. package/clients/viewer/viewer.css +433 -0
  34. package/logics_manager/assist.py +9 -142
  35. package/logics_manager/assist_handoff.py +132 -0
  36. package/logics_manager/assist_surface.py +38 -0
  37. package/logics_manager/cli.py +20 -0
  38. package/logics_manager/flow.py +126 -24
  39. package/logics_manager/flow_evidence.py +63 -0
  40. package/logics_manager/update_check.py +138 -0
  41. package/logics_manager/viewer.py +533 -0
  42. package/package.json +12 -6
  43. package/pyproject.toml +1 -1
@@ -0,0 +1,491 @@
1
+ (() => {
2
+ window.createCdxLogicsWebviewSelectors = function createCdxLogicsWebviewSelectors(options) {
3
+ const {
4
+ modelApi,
5
+ primaryStageOrder,
6
+ companionStageOrder,
7
+ compactListQuery,
8
+ getItems,
9
+ getSelectedId,
10
+ getActiveWorkspaceRoot,
11
+ getChangedPaths,
12
+ getActiveAgent,
13
+ getLastInjectedContext,
14
+ getHideCompleted,
15
+ getHideProcessedRequests,
16
+ getHideSpec,
17
+ getShowCompanionDocs,
18
+ getHideEmptyColumns,
19
+ getSearchQuery,
20
+ getGroupMode,
21
+ getSortMode,
22
+ getAttentionOnly,
23
+ getUiState
24
+ } = options;
25
+
26
+ function getStageLabel(stage) {
27
+ return typeof modelApi.getStageLabel === "function" ? modelApi.getStageLabel(stage) : String(stage || "item");
28
+ }
29
+
30
+ function getStageHeading(stage) {
31
+ return typeof modelApi.getStageHeading === "function" ? modelApi.getStageHeading(stage) : String(stage || "").trim();
32
+ }
33
+
34
+ function isCompactListForced() {
35
+ return Boolean(compactListQuery && compactListQuery.matches);
36
+ }
37
+
38
+ function getEffectiveViewMode() {
39
+ return isCompactListForced() ? "list" : getUiState().viewMode;
40
+ }
41
+
42
+ function isListMode() {
43
+ return getEffectiveViewMode() === "list";
44
+ }
45
+
46
+ function normalizeSearchValue(value) {
47
+ return String(value || "").trim().toLowerCase();
48
+ }
49
+
50
+ function getStatusValue(item) {
51
+ return normalizeSearchValue(item && item.indicators ? item.indicators.Status : "") || "no status";
52
+ }
53
+
54
+ function isPrimaryFlowStage(stage) {
55
+ return typeof modelApi.isPrimaryFlowStage === "function" ? modelApi.isPrimaryFlowStage(stage) : false;
56
+ }
57
+
58
+ function isCompanionStage(stage) {
59
+ return typeof modelApi.isCompanionStage === "function" ? modelApi.isCompanionStage(stage) : false;
60
+ }
61
+
62
+ function collectCompanionDocs(item) {
63
+ return typeof modelApi.collectCompanionDocs === "function" ? modelApi.collectCompanionDocs(item, getItems()) : [];
64
+ }
65
+
66
+ function collectSpecs(item) {
67
+ return typeof modelApi.collectSpecs === "function" ? modelApi.collectSpecs(item, getItems()) : [];
68
+ }
69
+
70
+ function collectPrimaryFlowItems(item) {
71
+ return typeof modelApi.collectPrimaryFlowItems === "function" ? modelApi.collectPrimaryFlowItems(item, getItems()) : [];
72
+ }
73
+
74
+ function collectLinkedWorkflowItems(item) {
75
+ if (typeof modelApi.collectLinkedWorkflowItems !== "function") {
76
+ throw new Error("collectLinkedWorkflowItems is not available on modelApi.");
77
+ }
78
+ return modelApi.collectLinkedWorkflowItems(item, getItems());
79
+ }
80
+
81
+ function getAttentionReasons(item) {
82
+ return typeof modelApi.getAttentionReasons === "function" ? modelApi.getAttentionReasons(item, getItems()) : [];
83
+ }
84
+
85
+ function buildContextPack(item, options = {}) {
86
+ return typeof modelApi.buildContextPack === "function"
87
+ ? modelApi.buildContextPack(item, getItems(), {
88
+ changedPaths: typeof getChangedPaths === "function" ? getChangedPaths() : [],
89
+ activeAgent: typeof getActiveAgent === "function" ? getActiveAgent() : null,
90
+ lastInjectedContext: typeof getLastInjectedContext === "function" ? getLastInjectedContext() : null,
91
+ currentRoot: typeof getActiveWorkspaceRoot === "function" ? getActiveWorkspaceRoot() : null,
92
+ ...options
93
+ })
94
+ : null;
95
+ }
96
+
97
+ function buildDependencyMap(item) {
98
+ return typeof modelApi.buildDependencyMap === "function" ? modelApi.buildDependencyMap(item, getItems()) : null;
99
+ }
100
+
101
+ function findManagedItemByReference(rawValue, fallbackUsage) {
102
+ return typeof modelApi.findManagedItemByReference === "function"
103
+ ? modelApi.findManagedItemByReference(rawValue, getItems(), fallbackUsage)
104
+ : null;
105
+ }
106
+
107
+ function normalizeManagedDocValue(value) {
108
+ return typeof modelApi.normalizeManagedDocValue === "function" ? modelApi.normalizeManagedDocValue(value) : String(value || "");
109
+ }
110
+
111
+ function getVisibleStages() {
112
+ const visibleStages = [...primaryStageOrder];
113
+ if (getShowCompanionDocs()) {
114
+ visibleStages.push(...companionStageOrder);
115
+ }
116
+ if (!getHideSpec()) {
117
+ visibleStages.push("spec");
118
+ }
119
+ return visibleStages;
120
+ }
121
+
122
+ function getProgressValue(item) {
123
+ const indicators = item && item.indicators ? item.indicators : {};
124
+ const progressKey = Object.keys(indicators).find((key) => key.toLowerCase().includes("progress"));
125
+ if (!progressKey) {
126
+ return null;
127
+ }
128
+ const progressRaw = indicators[progressKey];
129
+ const match = String(progressRaw).match(/(\d+(?:\.\d+)?)/);
130
+ if (!match) {
131
+ return null;
132
+ }
133
+ const value = Number(match[1]);
134
+ return Number.isFinite(value) ? value : null;
135
+ }
136
+
137
+ function isComplete(item) {
138
+ const value = getProgressValue(item);
139
+ return value !== null && value >= 100;
140
+ }
141
+
142
+ function normalizeStatus(value) {
143
+ return String(value || "")
144
+ .trim()
145
+ .toLowerCase();
146
+ }
147
+
148
+ function isProcessedWorkflowStatus(value) {
149
+ const normalized = normalizeStatus(value);
150
+ return normalized === "ready" || normalized === "done" || normalized === "complete" || normalized === "completed" || normalized === "archived";
151
+ }
152
+
153
+ function isClosedWorkflowStatus(value) {
154
+ const normalized = normalizeStatus(value);
155
+ return (
156
+ normalized === "done" ||
157
+ normalized === "complete" ||
158
+ normalized === "completed" ||
159
+ normalized === "archived" ||
160
+ normalized === "obsolete" ||
161
+ normalized === "superseded"
162
+ );
163
+ }
164
+
165
+ function isRequestProcessed(item) {
166
+ return typeof modelApi.isRequestProcessed === "function" ? modelApi.isRequestProcessed(item, getItems()) : false;
167
+ }
168
+
169
+ function progressState(item) {
170
+ const value = getProgressValue(item);
171
+ if (value === null) {
172
+ return "";
173
+ }
174
+ if (value <= 0) {
175
+ return "card--progress-zero";
176
+ }
177
+ if (value >= 100) {
178
+ return "card--progress-done";
179
+ }
180
+ return "card--progress-active";
181
+ }
182
+
183
+ function canPromote(item) {
184
+ if (!item) {
185
+ return false;
186
+ }
187
+ if (item.isPromoted) {
188
+ return false;
189
+ }
190
+ if (isRequestProcessed(item)) {
191
+ return false;
192
+ }
193
+ return item.stage === "request" || item.stage === "backlog";
194
+ }
195
+
196
+ function getHealthSignals(item) {
197
+ return getAttentionReasons(item).map((reason) => reason.key);
198
+ }
199
+
200
+ function needsAttention(item) {
201
+ return getAttentionReasons(item).length > 0;
202
+ }
203
+
204
+ function getSuggestedActions(item) {
205
+ const actions = [];
206
+ if (canPromote(item)) {
207
+ actions.push({ key: "promote", label: "Promote", title: "This workflow item can be promoted." });
208
+ }
209
+ if (isPrimaryFlowStage(item.stage) && collectCompanionDocs(item).length === 0 && collectSpecs(item).length === 0) {
210
+ actions.push({ key: "add-docs", label: "Add docs", title: "This workflow item needs companion docs or specs." });
211
+ }
212
+ if (!isPrimaryFlowStage(item.stage) && collectPrimaryFlowItems(item).length === 0) {
213
+ actions.push({
214
+ key: "link-flow",
215
+ label: "Link flow",
216
+ title: "This supporting doc should be linked back to a primary-flow item."
217
+ });
218
+ }
219
+ return actions.slice(0, 2);
220
+ }
221
+
222
+ function getActivityEntries() {
223
+ return [...getItems()]
224
+ .filter((item) => !(getHideCompleted() && (isComplete(item) || isClosedWorkflowStatus(getStatusValue(item)))))
225
+ .sort((left, right) => (Date.parse(right.updatedAt || "") || 0) - (Date.parse(left.updatedAt || "") || 0))
226
+ .slice(0, 12)
227
+ .map((item) => {
228
+ const statusValue = getStatusValue(item);
229
+ let label = "Updated";
230
+ if (statusValue.includes("obsolete")) {
231
+ label = "Marked obsolete";
232
+ } else if (statusValue.includes("done") || statusValue.includes("complete")) {
233
+ label = "Marked done";
234
+ } else if (item.isPromoted) {
235
+ label = "Promoted";
236
+ } else if (isPrimaryFlowStage(item.stage) && (collectCompanionDocs(item).length > 0 || collectSpecs(item).length > 0)) {
237
+ label = "Linked companion docs";
238
+ }
239
+ return {
240
+ id: item.id,
241
+ title: item.title,
242
+ stage: item.stage,
243
+ updatedAt: item.updatedAt,
244
+ label
245
+ };
246
+ });
247
+ }
248
+
249
+ function getHelpBannerMessage() {
250
+ if (getItems().length === 0) {
251
+ return "No Logics items are loaded yet. Use Tools > New Request or Bootstrap Logics to seed the workspace.";
252
+ }
253
+ if (!getSelectedId()) {
254
+ return "Select a card for details. Use Search to find items faster, Attention to triage, and List mode when the board gets crowded.";
255
+ }
256
+ return "";
257
+ }
258
+
259
+ function getProgressSortValue(item) {
260
+ const value = getProgressValue(item);
261
+ return typeof value === "number" ? value : -1;
262
+ }
263
+
264
+ function getEffectiveSortMode() {
265
+ return getSortMode() === "default" ? "updated-desc" : getSortMode();
266
+ }
267
+
268
+ function getStatusGroupOrder(value) {
269
+ const statusOrder = [
270
+ "blocked",
271
+ "draft",
272
+ "proposed",
273
+ "ready",
274
+ "in progress",
275
+ "accepted",
276
+ "validated",
277
+ "done",
278
+ "archived",
279
+ "obsolete",
280
+ "superseded",
281
+ "no status"
282
+ ];
283
+ const normalized = normalizeSearchValue(value) || "no status";
284
+ const index = statusOrder.indexOf(normalized);
285
+ return index === -1 ? statusOrder.length : index;
286
+ }
287
+
288
+ function compareItems(left, right) {
289
+ const sortMode = getEffectiveSortMode();
290
+ if (sortMode === "updated-desc") {
291
+ const leftTime = Date.parse(left.updatedAt || "") || 0;
292
+ const rightTime = Date.parse(right.updatedAt || "") || 0;
293
+ if (rightTime !== leftTime) {
294
+ return rightTime - leftTime;
295
+ }
296
+ } else if (sortMode === "progress-desc") {
297
+ const progressDelta = getProgressSortValue(right) - getProgressSortValue(left);
298
+ if (progressDelta !== 0) {
299
+ return progressDelta;
300
+ }
301
+ } else if (sortMode === "status-asc") {
302
+ const statusDelta = getStatusValue(left).localeCompare(getStatusValue(right));
303
+ if (statusDelta !== 0) {
304
+ return statusDelta;
305
+ }
306
+ } else if (sortMode === "title-asc") {
307
+ return normalizeSearchValue(left.title).localeCompare(normalizeSearchValue(right.title));
308
+ }
309
+
310
+ return normalizeSearchValue(left.title).localeCompare(normalizeSearchValue(right.title));
311
+ }
312
+
313
+ function sortItems(allItems) {
314
+ return [...allItems].sort(compareItems);
315
+ }
316
+
317
+ function groupByStage(allItems) {
318
+ const grouped = allItems.reduce((acc, item) => {
319
+ acc[item.stage] = acc[item.stage] || [];
320
+ acc[item.stage].push(item);
321
+ return acc;
322
+ }, {});
323
+ Object.keys(grouped).forEach((stage) => {
324
+ grouped[stage] = sortItems(grouped[stage]);
325
+ });
326
+ return grouped;
327
+ }
328
+
329
+ function collectSearchText(value) {
330
+ if (Array.isArray(value)) {
331
+ return value.map((entry) => collectSearchText(entry)).join(" ");
332
+ }
333
+ if (value && typeof value === "object") {
334
+ return Object.values(value)
335
+ .map((entry) => collectSearchText(entry))
336
+ .join(" ");
337
+ }
338
+ return String(value || "");
339
+ }
340
+
341
+ function matchesSearch(item) {
342
+ const normalizedQuery = normalizeSearchValue(getSearchQuery());
343
+ if (!normalizedQuery) {
344
+ return true;
345
+ }
346
+ const haystack = normalizeSearchValue(
347
+ [
348
+ item.title,
349
+ item.id,
350
+ item.stage,
351
+ getStageLabel(item.stage),
352
+ collectSearchText(item.references),
353
+ collectSearchText(item.usedBy),
354
+ collectSearchText(item.indicators)
355
+ ].join(" ")
356
+ );
357
+ return haystack.includes(normalizedQuery);
358
+ }
359
+
360
+ function isVisible(item) {
361
+ if (typeof window.__CDX_LOGICS_VIEWER_FILTER__ === "function" && !window.__CDX_LOGICS_VIEWER_FILTER__(item)) {
362
+ return false;
363
+ }
364
+ if (getAttentionOnly() && !needsAttention(item)) {
365
+ return false;
366
+ }
367
+ if (getHideCompleted() && (isComplete(item) || isClosedWorkflowStatus(getStatusValue(item)))) {
368
+ return false;
369
+ }
370
+ if (getHideProcessedRequests() && item.stage === "request" && isRequestProcessed(item)) {
371
+ return false;
372
+ }
373
+ if (getHideSpec() && item.stage === "spec") {
374
+ return false;
375
+ }
376
+ if (!getShowCompanionDocs() && isCompanionStage(item.stage)) {
377
+ return false;
378
+ }
379
+ return matchesSearch(item);
380
+ }
381
+
382
+ function getListGroups() {
383
+ const visibleItems = getItems().filter((item) => isVisible(item));
384
+ if (getGroupMode() === "status") {
385
+ const grouped = visibleItems.reduce((acc, item) => {
386
+ const heading = item && item.indicators && item.indicators.Status ? String(item.indicators.Status) : "No status";
387
+ const key = `status:${normalizeSearchValue(heading) || "no-status"}`;
388
+ acc[key] = acc[key] || { key, heading, items: [], totalCount: 0 };
389
+ acc[key].items.push(item);
390
+ acc[key].totalCount = acc[key].items.length;
391
+ return acc;
392
+ }, {});
393
+ return Object.values(grouped)
394
+ .map((group) => ({ ...group, items: sortItems(group.items), totalCount: group.items.length }))
395
+ .sort((left, right) => {
396
+ const leftIndex = getStatusGroupOrder(normalizeSearchValue(left.heading));
397
+ const rightIndex = getStatusGroupOrder(normalizeSearchValue(right.heading));
398
+ if (leftIndex !== rightIndex) {
399
+ return leftIndex - rightIndex;
400
+ }
401
+ return normalizeSearchValue(left.heading).localeCompare(normalizeSearchValue(right.heading));
402
+ });
403
+ }
404
+
405
+ const grouped = groupByStage(visibleItems);
406
+ return getVisibleStages()
407
+ .map((stage) => ({
408
+ key: stage,
409
+ stage,
410
+ heading: getStageHeading(stage),
411
+ items: grouped[stage] || [],
412
+ totalCount: (grouped[stage] || []).length,
413
+ emptyLabel: isPrimaryFlowStage(stage) ? "No items" : "No linked docs"
414
+ }))
415
+ .filter((group) => !getHideEmptyColumns() || group.items.length > 0);
416
+ }
417
+
418
+ function formatDate(value) {
419
+ if (!value) {
420
+ return "-";
421
+ }
422
+ const date = new Date(value);
423
+ if (Number.isNaN(date.getTime())) {
424
+ return value;
425
+ }
426
+ const diffMs = Date.now() - date.getTime();
427
+ if (diffMs >= 0 && diffMs < 24 * 60 * 60 * 1000) {
428
+ const totalMinutes = Math.max(1, Math.round(diffMs / (60 * 1000)));
429
+ const hours = Math.floor(totalMinutes / 60);
430
+ const minutes = totalMinutes % 60;
431
+ const relativeLabel =
432
+ hours > 0
433
+ ? minutes > 0
434
+ ? `${hours}h ${minutes}m ago`
435
+ : `${hours}h ago`
436
+ : `${totalMinutes}m ago`;
437
+ const preciseTime = new Intl.DateTimeFormat(undefined, {
438
+ hour: "2-digit",
439
+ minute: "2-digit"
440
+ }).format(date);
441
+ return `${relativeLabel} • ${preciseTime}`;
442
+ }
443
+ return date.toLocaleString();
444
+ }
445
+
446
+ return {
447
+ canPromote,
448
+ isCompactListForced,
449
+ getEffectiveViewMode,
450
+ isListMode,
451
+ getStageLabel,
452
+ getStageHeading,
453
+ normalizeSearchValue,
454
+ getStatusValue,
455
+ getAttentionReasons,
456
+ getHealthSignals,
457
+ buildContextPack,
458
+ buildDependencyMap,
459
+ needsAttention,
460
+ getSuggestedActions,
461
+ getActivityEntries,
462
+ getHelpBannerMessage,
463
+ getProgressSortValue,
464
+ compareItems,
465
+ sortItems,
466
+ groupByStage,
467
+ collectSearchText,
468
+ matchesSearch,
469
+ isVisible,
470
+ getListGroups,
471
+ isPrimaryFlowStage,
472
+ isCompanionStage,
473
+ collectCompanionDocs,
474
+ collectSpecs,
475
+ collectPrimaryFlowItems,
476
+ findManagedItemByReference,
477
+ normalizeManagedDocValue,
478
+ getVisibleStages,
479
+ isComplete,
480
+ formatDate,
481
+ normalizeStatus,
482
+ isProcessedWorkflowStatus,
483
+ isClosedWorkflowStatus,
484
+ collectLinkedWorkflowItems,
485
+ isRequestProcessed,
486
+ getEffectiveSortMode,
487
+ progressState,
488
+ getProgressValue
489
+ };
490
+ };
491
+ })();
@@ -0,0 +1,5 @@
1
+ # Local Viewer Client
2
+
3
+ Reserved for the CLI-launched local browser viewer.
4
+
5
+ The viewer should consume `clients/shared-web/` through a browser host adapter instead of duplicating the VS Code webview runtime.