@grifhinz/logics-manager 2.1.2 → 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 (49) hide show
  1. package/README.md +106 -4
  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 +94 -63
  35. package/logics_manager/assist_handoff.py +132 -0
  36. package/logics_manager/assist_surface.py +38 -0
  37. package/logics_manager/cli.py +152 -12
  38. package/logics_manager/cli_output.py +18 -0
  39. package/logics_manager/flow.py +1360 -84
  40. package/logics_manager/flow_evidence.py +63 -0
  41. package/logics_manager/index.py +3 -7
  42. package/logics_manager/insights.py +418 -0
  43. package/logics_manager/mcp.py +50 -0
  44. package/logics_manager/path_utils.py +31 -0
  45. package/logics_manager/sync.py +24 -12
  46. package/logics_manager/update_check.py +138 -0
  47. package/logics_manager/viewer.py +533 -0
  48. package/package.json +12 -6
  49. package/pyproject.toml +1 -1
@@ -0,0 +1,910 @@
1
+ (() => {
2
+ const WORKFLOW_STAGE_ORDER = ["request", "backlog", "task"];
3
+
4
+ function getStageLabel(stage) {
5
+ switch (stage) {
6
+ case "request":
7
+ return "request";
8
+ case "backlog":
9
+ return "backlog";
10
+ case "task":
11
+ return "task";
12
+ case "product":
13
+ return "product brief";
14
+ case "architecture":
15
+ return "architecture decision";
16
+ case "spec":
17
+ return "spec";
18
+ default:
19
+ return String(stage || "item");
20
+ }
21
+ }
22
+
23
+ function getStageHeading(stage) {
24
+ switch (stage) {
25
+ case "request":
26
+ return "Requests";
27
+ case "backlog":
28
+ return "Backlog";
29
+ case "task":
30
+ return "Tasks";
31
+ case "product":
32
+ return "Product briefs";
33
+ case "architecture":
34
+ return "Architecture decisions";
35
+ case "spec":
36
+ return "Specs";
37
+ default:
38
+ return String(stage || "").trim();
39
+ }
40
+ }
41
+
42
+ function isPrimaryFlowStage(stage) {
43
+ return stage === "request" || stage === "backlog" || stage === "task";
44
+ }
45
+
46
+ function isCompanionStage(stage) {
47
+ return stage === "product" || stage === "architecture";
48
+ }
49
+
50
+ function normalizeManagedDocValue(value) {
51
+ return String(value || "")
52
+ .replace(/\\/g, "/")
53
+ .replace(/^\.?\//, "")
54
+ .trim();
55
+ }
56
+
57
+ function inferManagedDocId(normalizedValue, fallbackUsage) {
58
+ if (fallbackUsage && typeof fallbackUsage.id === "string" && fallbackUsage.id) {
59
+ return fallbackUsage.id;
60
+ }
61
+ if (!normalizedValue) {
62
+ return "";
63
+ }
64
+ return normalizedValue.split("/").pop()?.replace(/\.md$/i, "") || normalizedValue;
65
+ }
66
+
67
+ function inferCompanionStage(normalizedValue, fallbackUsage) {
68
+ if (fallbackUsage && isCompanionStage(fallbackUsage.stage)) {
69
+ return fallbackUsage.stage;
70
+ }
71
+ const fileStem = inferManagedDocId(normalizedValue);
72
+ if (normalizedValue.startsWith("logics/product/") || fileStem.startsWith("prod_")) {
73
+ return "product";
74
+ }
75
+ if (normalizedValue.startsWith("logics/architecture/") || fileStem.startsWith("adr_")) {
76
+ return "architecture";
77
+ }
78
+ return null;
79
+ }
80
+
81
+ function findManagedItemByReference(rawValue, allItems, fallbackUsage) {
82
+ const normalizedValue = normalizeManagedDocValue(rawValue);
83
+ if (fallbackUsage && fallbackUsage.id) {
84
+ const byUsageId = allItems.find((entry) => entry.id === fallbackUsage.id);
85
+ if (byUsageId) {
86
+ return byUsageId;
87
+ }
88
+ }
89
+ if (!normalizedValue) {
90
+ return null;
91
+ }
92
+ const fileStem = normalizedValue.split("/").pop()?.replace(/\.md$/i, "") || "";
93
+ return (
94
+ allItems.find((entry) => entry.relPath === normalizedValue) ||
95
+ allItems.find((entry) => entry.id === normalizedValue) ||
96
+ allItems.find((entry) => entry.id === fileStem) ||
97
+ null
98
+ );
99
+ }
100
+
101
+ function resolveCompanionFromValue(rawValue, allItems, fallbackUsage) {
102
+ const normalizedValue = normalizeManagedDocValue(rawValue);
103
+ const matchedItem = findManagedItemByReference(normalizedValue, allItems, fallbackUsage);
104
+ if (matchedItem && isCompanionStage(matchedItem.stage)) {
105
+ return {
106
+ id: matchedItem.id,
107
+ title: matchedItem.title,
108
+ stage: matchedItem.stage,
109
+ relPath: matchedItem.relPath,
110
+ item: matchedItem
111
+ };
112
+ }
113
+
114
+ const fallbackStage = inferCompanionStage(normalizedValue, fallbackUsage);
115
+ if (!fallbackStage) {
116
+ return null;
117
+ }
118
+
119
+ const fallbackId = inferManagedDocId(normalizedValue, fallbackUsage);
120
+ return {
121
+ id: fallbackId || normalizedValue,
122
+ title: fallbackUsage && fallbackUsage.title ? fallbackUsage.title : normalizedValue,
123
+ stage: fallbackStage,
124
+ relPath: normalizedValue,
125
+ item: null
126
+ };
127
+ }
128
+
129
+ function collectCompanionDocs(item, allItems) {
130
+ const companions = new Map();
131
+
132
+ const registerCompanion = (candidate) => {
133
+ if (!candidate || !isCompanionStage(candidate.stage)) {
134
+ return;
135
+ }
136
+ const key = candidate.relPath || candidate.id;
137
+ if (!key || companions.has(key)) {
138
+ return;
139
+ }
140
+ companions.set(key, candidate);
141
+ };
142
+
143
+ (item.references || []).forEach((reference) => {
144
+ if (!reference || typeof reference !== "object") {
145
+ return;
146
+ }
147
+ registerCompanion(resolveCompanionFromValue(reference.path, allItems));
148
+ });
149
+
150
+ (item.usedBy || []).forEach((usage) => {
151
+ registerCompanion(resolveCompanionFromValue(usage.relPath || usage.id, allItems, usage));
152
+ });
153
+
154
+ const order = ["product", "architecture"];
155
+ return Array.from(companions.values()).sort((left, right) => {
156
+ const leftIndex = order.indexOf(left.stage);
157
+ const rightIndex = order.indexOf(right.stage);
158
+ if (leftIndex !== rightIndex) {
159
+ return leftIndex - rightIndex;
160
+ }
161
+ return String(left.id).localeCompare(String(right.id));
162
+ });
163
+ }
164
+
165
+ function collectSpecs(item, allItems) {
166
+ const specs = new Map();
167
+
168
+ const registerSpec = (candidate) => {
169
+ if (!candidate || candidate.stage !== "spec") {
170
+ return;
171
+ }
172
+ const key = candidate.relPath || candidate.id;
173
+ if (!key || specs.has(key)) {
174
+ return;
175
+ }
176
+ specs.set(key, candidate);
177
+ };
178
+
179
+ (item.references || []).forEach((reference) => {
180
+ if (!reference || typeof reference !== "object") {
181
+ return;
182
+ }
183
+ registerSpec(findManagedItemByReference(reference.path, allItems));
184
+ });
185
+
186
+ (item.usedBy || []).forEach((usage) => {
187
+ registerSpec(findManagedItemByReference(usage.relPath || usage.id, allItems, usage));
188
+ });
189
+
190
+ return Array.from(specs.values()).sort((left, right) => String(left.id).localeCompare(String(right.id)));
191
+ }
192
+
193
+ function collectPrimaryFlowItems(item, allItems) {
194
+ const linkedItems = new Map();
195
+
196
+ const registerItem = (candidate) => {
197
+ if (!candidate || !isPrimaryFlowStage(candidate.stage)) {
198
+ return;
199
+ }
200
+ const key = candidate.relPath || candidate.id;
201
+ if (!key || linkedItems.has(key)) {
202
+ return;
203
+ }
204
+ linkedItems.set(key, candidate);
205
+ };
206
+
207
+ (item.references || []).forEach((reference) => {
208
+ if (!reference || typeof reference !== "object") {
209
+ return;
210
+ }
211
+ registerItem(findManagedItemByReference(reference.path, allItems));
212
+ });
213
+
214
+ (item.usedBy || []).forEach((usage) => {
215
+ registerItem(findManagedItemByReference(usage.relPath || usage.id, allItems, usage));
216
+ });
217
+
218
+ const order = ["request", "backlog", "task"];
219
+ return Array.from(linkedItems.values()).sort((left, right) => {
220
+ const leftIndex = order.indexOf(left.stage);
221
+ const rightIndex = order.indexOf(right.stage);
222
+ if (leftIndex !== rightIndex) {
223
+ return leftIndex - rightIndex;
224
+ }
225
+ return String(left.id).localeCompare(String(right.id));
226
+ });
227
+ }
228
+
229
+ function normalizeStatus(value) {
230
+ return String(value || "")
231
+ .trim()
232
+ .toLowerCase();
233
+ }
234
+
235
+ function parseProgress(value) {
236
+ const match = String(value || "").match(/(\d+(?:\.\d+)?)/);
237
+ if (!match) {
238
+ return null;
239
+ }
240
+ const parsed = Number(match[1]);
241
+ if (!Number.isFinite(parsed)) {
242
+ return null;
243
+ }
244
+ return Math.max(0, Math.min(100, parsed));
245
+ }
246
+
247
+ function isProcessedWorkflowStatus(value) {
248
+ const normalized = normalizeStatus(value);
249
+ return normalized === "ready" || normalized === "done" || normalized === "complete" || normalized === "completed" || normalized === "archived";
250
+ }
251
+
252
+ function isProcessedWorkflowItem(item) {
253
+ if (!item) {
254
+ return false;
255
+ }
256
+ if (item.stage !== "backlog" && item.stage !== "task") {
257
+ return false;
258
+ }
259
+ if (isProcessedWorkflowStatus(item && item.indicators ? item.indicators.Status : "")) {
260
+ return true;
261
+ }
262
+ return parseProgress(item && item.indicators ? item.indicators.Progress : "") === 100;
263
+ }
264
+
265
+ function workflowCandidateKeys(candidate) {
266
+ const keys = new Set();
267
+ if (candidate && candidate.relPath) {
268
+ const normalizedPath = normalizeManagedDocValue(candidate.relPath);
269
+ keys.add(normalizedPath);
270
+ keys.add(normalizedPath.split("/").pop()?.replace(/\.md$/i, "") || normalizedPath);
271
+ }
272
+ if (candidate && candidate.id) {
273
+ keys.add(candidate.id);
274
+ }
275
+ return Array.from(keys).filter(Boolean);
276
+ }
277
+
278
+ function collectLinkedWorkflowItems(item, allItems) {
279
+ if (!item || item.stage !== "request") {
280
+ return [];
281
+ }
282
+ const linkedValues = new Set();
283
+ if (Array.isArray(item.references)) {
284
+ item.references.forEach((ref) => {
285
+ if (ref && typeof ref.path === "string") {
286
+ linkedValues.add(normalizeManagedDocValue(ref.path));
287
+ }
288
+ });
289
+ }
290
+ if (Array.isArray(item.usedBy)) {
291
+ item.usedBy.forEach((usage) => {
292
+ const rawValue =
293
+ usage && typeof usage.relPath === "string"
294
+ ? usage.relPath
295
+ : usage && typeof usage.id === "string"
296
+ ? usage.id
297
+ : "";
298
+ if (rawValue) {
299
+ linkedValues.add(normalizeManagedDocValue(rawValue));
300
+ }
301
+ });
302
+ }
303
+ const linkedItems = new Map();
304
+ (allItems || []).forEach((candidate) => {
305
+ workflowCandidateKeys(candidate).forEach((key) => {
306
+ if (!linkedItems.has(key)) {
307
+ linkedItems.set(key, candidate);
308
+ }
309
+ });
310
+ });
311
+ return Array.from(linkedValues)
312
+ .map((rawValue) => linkedItems.get(rawValue) || findManagedItemByReference(rawValue, allItems))
313
+ .filter((candidate, index, collection) => candidate && collection.indexOf(candidate) === index);
314
+ }
315
+
316
+ function isRequestProcessed(item, allItems) {
317
+ if (!item || item.stage !== "request") {
318
+ return false;
319
+ }
320
+ if (!isProcessedWorkflowStatus(item && item.indicators ? item.indicators.Status : "")) {
321
+ return false;
322
+ }
323
+ return collectLinkedWorkflowItems(item, allItems).some((candidate) => isProcessedWorkflowItem(candidate));
324
+ }
325
+
326
+ function getWorkflowStageRank(stage) {
327
+ return WORKFLOW_STAGE_ORDER.indexOf(stage);
328
+ }
329
+
330
+ function dedupeItems(items) {
331
+ const seen = new Set();
332
+ return (items || []).filter((item) => {
333
+ if (!item) {
334
+ return false;
335
+ }
336
+ const key = item.relPath || item.id;
337
+ if (!key || seen.has(key)) {
338
+ return false;
339
+ }
340
+ seen.add(key);
341
+ return true;
342
+ });
343
+ }
344
+
345
+ function sortWorkflowItems(items) {
346
+ return dedupeItems(items).sort((left, right) => {
347
+ const leftRank = getWorkflowStageRank(left.stage);
348
+ const rightRank = getWorkflowStageRank(right.stage);
349
+ if (leftRank !== rightRank) {
350
+ return leftRank - rightRank;
351
+ }
352
+ return String(left.id).localeCompare(String(right.id));
353
+ });
354
+ }
355
+
356
+ function sortManagedItems(items) {
357
+ return dedupeItems(items).sort((left, right) => {
358
+ const leftStage = getStageLabel(left.stage);
359
+ const rightStage = getStageLabel(right.stage);
360
+ if (leftStage !== rightStage) {
361
+ return leftStage.localeCompare(rightStage);
362
+ }
363
+ return String(left.id).localeCompare(String(right.id));
364
+ });
365
+ }
366
+
367
+ function getRelationshipInsights(item, allItems) {
368
+ const companions = collectCompanionDocs(item, allItems);
369
+ const specs = collectSpecs(item, allItems);
370
+ const primaryFlowLinks = collectPrimaryFlowItems(item, allItems);
371
+ const stageRank = getWorkflowStageRank(item && item.stage);
372
+
373
+ let upstream = [];
374
+ let downstream = [];
375
+ let linkedWorkflow = [];
376
+
377
+ if (stageRank >= 0) {
378
+ upstream = sortWorkflowItems(primaryFlowLinks.filter((candidate) => getWorkflowStageRank(candidate.stage) < stageRank));
379
+ downstream = sortWorkflowItems(primaryFlowLinks.filter((candidate) => getWorkflowStageRank(candidate.stage) > stageRank));
380
+ } else {
381
+ linkedWorkflow = sortWorkflowItems(primaryFlowLinks);
382
+ }
383
+
384
+ return {
385
+ upstream,
386
+ downstream,
387
+ linkedWorkflow,
388
+ companionDocs: sortManagedItems(companions.map((entry) => entry.item || entry).filter(Boolean)),
389
+ specs: sortManagedItems(specs),
390
+ supportingDocs: sortManagedItems([
391
+ ...companions.map((entry) => entry.item || entry).filter(Boolean),
392
+ ...specs
393
+ ])
394
+ };
395
+ }
396
+
397
+ function getAttentionReasons(item, allItems) {
398
+ if (!item) {
399
+ return [];
400
+ }
401
+
402
+ const insights = getRelationshipInsights(item, allItems);
403
+ const statusValue = normalizeStatus(item && item.indicators ? item.indicators.Status : "");
404
+ const progressValue = parseProgress(item && item.indicators ? item.indicators.Progress : "");
405
+ const reasons = [];
406
+
407
+ if (statusValue.includes("blocked")) {
408
+ reasons.push({
409
+ key: "blocked",
410
+ label: "Blocked",
411
+ shortLabel: "Blocked",
412
+ description: "This item is explicitly marked as blocked in its indicators.",
413
+ remediation: {
414
+ label: "Update blockers in the doc",
415
+ description: "Clarify the blocking dependency or move the item back to an actionable status."
416
+ }
417
+ });
418
+ }
419
+
420
+ if (progressValue === 100 && !statusValue.includes("done") && !statusValue.includes("complete")) {
421
+ reasons.push({
422
+ key: "workflow-inconsistent",
423
+ label: "Workflow inconsistent",
424
+ shortLabel: "Inconsistent",
425
+ description: "Progress is at 100% but the workflow status is not marked as done or complete.",
426
+ remediation: {
427
+ label: "Sync status with progress",
428
+ description: "Mark the item done or adjust progress so status and progress describe the same state."
429
+ }
430
+ });
431
+ }
432
+
433
+ if (
434
+ item.stage === "request" &&
435
+ !collectLinkedWorkflowItems(item, allItems).some((candidate) => candidate.stage === "backlog" || candidate.stage === "task")
436
+ ) {
437
+ reasons.push({
438
+ key: "workflow-inconsistent",
439
+ label: "Workflow inconsistent",
440
+ shortLabel: "No delivery child",
441
+ description: "This request has no linked backlog or task item yet.",
442
+ remediation: {
443
+ label: "Promote request",
444
+ action: "promote"
445
+ }
446
+ });
447
+ }
448
+
449
+ if (!isPrimaryFlowStage(item.stage) && insights.linkedWorkflow.length === 0) {
450
+ reasons.push({
451
+ key: "orphaned",
452
+ label: "Orphaned",
453
+ shortLabel: "Orphaned",
454
+ description: "This supporting document is not linked back to any request, backlog item, or task.",
455
+ remediation: {
456
+ label: "Link to primary flow",
457
+ action: "add-reference"
458
+ }
459
+ });
460
+ }
461
+
462
+ const priority = {
463
+ blocked: 0,
464
+ "workflow-inconsistent": 1,
465
+ orphaned: 2
466
+ };
467
+
468
+ return reasons
469
+ .sort((left, right) => {
470
+ const leftPriority = Object.prototype.hasOwnProperty.call(priority, left.key) ? priority[left.key] : 99;
471
+ const rightPriority = Object.prototype.hasOwnProperty.call(priority, right.key) ? priority[right.key] : 99;
472
+ if (leftPriority !== rightPriority) {
473
+ return leftPriority - rightPriority;
474
+ }
475
+ return String(left.label).localeCompare(String(right.label));
476
+ })
477
+ .filter((reason, index, collection) => collection.findIndex((candidate) => candidate.key === reason.key && candidate.description === reason.description) === index);
478
+ }
479
+
480
+ function describeContextItem(item) {
481
+ const status = item && item.indicators && item.indicators.Status ? ` [${item.indicators.Status}]` : "";
482
+ return `${getStageLabel(item.stage)} • ${item.id} — ${item.title}${status} (${item.relPath})`;
483
+ }
484
+
485
+ function renderContextSection(title, items) {
486
+ const safeItems = (items || []).filter(Boolean);
487
+ if (safeItems.length === 0) {
488
+ return [`## ${title}`, "- (none)"];
489
+ }
490
+ return [`## ${title}`, ...safeItems.map((item) => `- ${describeContextItem(item)}`)];
491
+ }
492
+
493
+ function normalizeContextMode(value) {
494
+ const normalized = String(value || "standard").trim().toLowerCase();
495
+ if (normalized === "summary-only" || normalized === "diff-first") {
496
+ return normalized;
497
+ }
498
+ return "standard";
499
+ }
500
+
501
+ function normalizeContextProfile(value) {
502
+ const normalized = String(value || "normal").trim().toLowerCase();
503
+ if (normalized === "tiny" || normalized === "deep") {
504
+ return normalized;
505
+ }
506
+ return "normal";
507
+ }
508
+
509
+ function normalizeResponseStyle(value) {
510
+ const normalized = String(value || "concise").trim().toLowerCase();
511
+ if (normalized === "balanced" || normalized === "detailed") {
512
+ return normalized;
513
+ }
514
+ return "concise";
515
+ }
516
+
517
+ function inferTaskKind(item, mode) {
518
+ if (mode === "diff-first") {
519
+ return item && item.stage === "spec" ? "review" : "implementation";
520
+ }
521
+ if (!item || item.stage === "request") {
522
+ return "request";
523
+ }
524
+ if (item.stage === "backlog" || item.stage === "task") {
525
+ return "implementation";
526
+ }
527
+ if (item.stage === "spec") {
528
+ return "spec";
529
+ }
530
+ return "review";
531
+ }
532
+
533
+ function inferDefaultProfile(item, activeAgent, mode, taskKind) {
534
+ if (activeAgent && activeAgent.preferredContextProfile) {
535
+ return normalizeContextProfile(activeAgent.preferredContextProfile);
536
+ }
537
+ if (mode === "summary-only") {
538
+ return "tiny";
539
+ }
540
+ if (mode === "diff-first") {
541
+ return "tiny";
542
+ }
543
+ if (taskKind === "spec") {
544
+ return "deep";
545
+ }
546
+ if (item && item.stage === "request") {
547
+ return "normal";
548
+ }
549
+ return "normal";
550
+ }
551
+
552
+ function buildProfileLimits(profile, mode) {
553
+ if (mode === "summary-only") {
554
+ return { upstream: 1, downstream: 1, linkedWorkflow: 2, companion: 1, specs: 1, summaryPoints: 3, acceptanceCriteria: 3, changedPaths: 6 };
555
+ }
556
+ if (mode === "diff-first") {
557
+ return { upstream: 1, downstream: 2, linkedWorkflow: 2, companion: 1, specs: 1, summaryPoints: 2, acceptanceCriteria: 4, changedPaths: 12 };
558
+ }
559
+ if (profile === "tiny") {
560
+ return { upstream: 1, downstream: 2, linkedWorkflow: 2, companion: 1, specs: 1, summaryPoints: 3, acceptanceCriteria: 3, changedPaths: 6 };
561
+ }
562
+ if (profile === "deep") {
563
+ return { upstream: 3, downstream: 4, linkedWorkflow: 4, companion: 4, specs: 3, summaryPoints: 5, acceptanceCriteria: 6, changedPaths: 16 };
564
+ }
565
+ return { upstream: 2, downstream: 3, linkedWorkflow: 3, companion: 3, specs: 2, summaryPoints: 4, acceptanceCriteria: 4, changedPaths: 10 };
566
+ }
567
+
568
+ function isCompleteStatus(item) {
569
+ const status = String(item && item.indicators && item.indicators.Status ? item.indicators.Status : "").trim().toLowerCase();
570
+ return status === "done" || status === "archived" || status === "obsolete" || status === "superseded";
571
+ }
572
+
573
+ function isWeaklyLinked(item, currentItem) {
574
+ if (!item || !currentItem || item.id === currentItem.id) {
575
+ return false;
576
+ }
577
+ if (item.stage === "product" || item.stage === "architecture" || item.stage === "spec") {
578
+ return true;
579
+ }
580
+ return false;
581
+ }
582
+
583
+ function allowDocStage(item, activeAgent) {
584
+ if (!item || !activeAgent) {
585
+ return true;
586
+ }
587
+ const stage = String(item.stage || "").trim().toLowerCase();
588
+ if (Array.isArray(activeAgent.blockedDocStages) && activeAgent.blockedDocStages.includes(stage)) {
589
+ return false;
590
+ }
591
+ if (Array.isArray(activeAgent.allowedDocStages) && activeAgent.allowedDocStages.length > 0) {
592
+ return activeAgent.allowedDocStages.includes(stage);
593
+ }
594
+ return true;
595
+ }
596
+
597
+ function filterContextItems(items, currentItem, activeAgent) {
598
+ const included = [];
599
+ let staleExcluded = 0;
600
+ let blockedExcluded = 0;
601
+ (items || []).forEach((candidate) => {
602
+ if (!candidate) {
603
+ return;
604
+ }
605
+ if (!allowDocStage(candidate, activeAgent)) {
606
+ blockedExcluded += 1;
607
+ return;
608
+ }
609
+ if (candidate.id !== currentItem.id && isCompleteStatus(candidate) && isWeaklyLinked(candidate, currentItem)) {
610
+ staleExcluded += 1;
611
+ return;
612
+ }
613
+ included.push(candidate);
614
+ });
615
+ return { included, staleExcluded, blockedExcluded };
616
+ }
617
+
618
+ function sliceContextItems(items, limit) {
619
+ return (items || []).filter(Boolean).slice(0, Math.max(0, limit || 0));
620
+ }
621
+
622
+ function getSummaryPoints(item, limit) {
623
+ const summaryPoints = Array.isArray(item && item.summaryPoints) ? item.summaryPoints : [];
624
+ return summaryPoints.filter(Boolean).slice(0, Math.max(0, limit || 0));
625
+ }
626
+
627
+ function getAcceptanceCriteria(item, limit) {
628
+ const criteria = Array.isArray(item && item.acceptanceCriteria) ? item.acceptanceCriteria : [];
629
+ return criteria.filter(Boolean).slice(0, Math.max(0, limit || 0));
630
+ }
631
+
632
+ function buildResponseContract(taskKind, responseStyle) {
633
+ const style = normalizeResponseStyle(responseStyle);
634
+ if (taskKind === "review") {
635
+ return style === "detailed"
636
+ ? "Review mode: findings first, then open questions, then a brief change summary."
637
+ : "Review mode: findings first. Keep the response terse unless deeper analysis is requested.";
638
+ }
639
+ if (taskKind === "implementation") {
640
+ return style === "detailed"
641
+ ? "Implementation mode: give the smallest complete fix, then a short verification note."
642
+ : "Implementation mode: respond concisely with the concrete change and a brief verification note.";
643
+ }
644
+ if (taskKind === "spec") {
645
+ return style === "concise"
646
+ ? "Spec mode: keep the structure clear and compact, and avoid unnecessary prose."
647
+ : "Spec mode: stay structured and avoid repeating context already present in the Logics docs.";
648
+ }
649
+ return style === "detailed"
650
+ ? "Default mode: stay grounded in the provided context and avoid repeating obvious repository history."
651
+ : "Default mode: respond briefly unless more depth is explicitly requested.";
652
+ }
653
+
654
+ function estimateTokens(charCount) {
655
+ return Math.max(1, Math.ceil(Number(charCount || 0) / 4));
656
+ }
657
+
658
+ function classifyBudget(tokenEstimate) {
659
+ if (tokenEstimate <= 180) {
660
+ return "Lean";
661
+ }
662
+ if (tokenEstimate <= 420) {
663
+ return "Medium";
664
+ }
665
+ return "Heavy";
666
+ }
667
+
668
+ function normalizeChangedPaths(paths, limit) {
669
+ return (Array.isArray(paths) ? paths : [])
670
+ .map((entry) => String(entry || "").replace(/\\/g, "/").trim())
671
+ .filter((entry, index, collection) => entry.length > 0 && collection.indexOf(entry) === index)
672
+ .slice(0, Math.max(0, limit || 0));
673
+ }
674
+
675
+ function getRelevantChangedPaths(item, insights, changedPaths, limit) {
676
+ const relatedPathHints = new Set([
677
+ String(item && item.relPath ? item.relPath : "").replace(/\\/g, "/"),
678
+ ...((insights.upstream || []).map((entry) => String(entry.relPath || "").replace(/\\/g, "/"))),
679
+ ...((insights.downstream || []).map((entry) => String(entry.relPath || "").replace(/\\/g, "/"))),
680
+ ...((insights.linkedWorkflow || []).map((entry) => String(entry.relPath || "").replace(/\\/g, "/"))),
681
+ ...((insights.supportingDocs || []).map((entry) => String(entry.relPath || "").replace(/\\/g, "/")))
682
+ ]);
683
+
684
+ const matching = normalizeChangedPaths(changedPaths, 80).filter((entry) => {
685
+ if (relatedPathHints.has(entry)) {
686
+ return true;
687
+ }
688
+ return Array.from(relatedPathHints).some((hint) => hint && entry.endsWith(pathBasename(hint)));
689
+ });
690
+
691
+ const fallback = matching.length > 0 ? matching : normalizeChangedPaths(changedPaths, limit);
692
+ return fallback.slice(0, Math.max(0, limit || 0));
693
+ }
694
+
695
+ function pathBasename(value) {
696
+ const normalized = String(value || "").replace(/\\/g, "/");
697
+ const segments = normalized.split("/");
698
+ return segments[segments.length - 1] || normalized;
699
+ }
700
+
701
+ function buildSessionHint(item, mode, taskKind, currentRoot, lastInjectedContext) {
702
+ if (!lastInjectedContext) {
703
+ return null;
704
+ }
705
+ if (lastInjectedContext.root && currentRoot && lastInjectedContext.root !== currentRoot) {
706
+ return "Fresh session recommended: the active repository root changed since the last assistant handoff.";
707
+ }
708
+ if (lastInjectedContext.itemId && lastInjectedContext.itemId !== item.id) {
709
+ if (lastInjectedContext.taskKind && lastInjectedContext.taskKind !== taskKind) {
710
+ return "Fresh session recommended: the active task type changed since the last assistant handoff.";
711
+ }
712
+ return "Fresh session recommended: you switched to a different Logics item since the last assistant handoff.";
713
+ }
714
+ if (lastInjectedContext.mode && lastInjectedContext.mode !== mode) {
715
+ return "Fresh session recommended: the handoff mode changed materially since the last assistant handoff.";
716
+ }
717
+ return null;
718
+ }
719
+
720
+ function buildContextPack(item, allItems, options) {
721
+ const safeOptions = options || {};
722
+ const activeAgent = safeOptions.activeAgent || null;
723
+ const mode = normalizeContextMode(safeOptions.mode);
724
+ const taskKind = inferTaskKind(item, mode);
725
+ const profile = normalizeContextProfile(safeOptions.profile || inferDefaultProfile(item, activeAgent, mode, taskKind));
726
+ const limits = buildProfileLimits(profile, mode);
727
+ const insights = getRelationshipInsights(item, allItems);
728
+ const attentionReasons = getAttentionReasons(item, allItems).slice(0, 3);
729
+ const upstreamState = filterContextItems(insights.upstream, item, activeAgent);
730
+ const downstreamState = filterContextItems(insights.downstream, item, activeAgent);
731
+ const linkedWorkflowState = filterContextItems(insights.linkedWorkflow, item, activeAgent);
732
+ const companionState = filterContextItems(insights.companionDocs, item, activeAgent);
733
+ const specsState = filterContextItems(insights.specs, item, activeAgent);
734
+ const upstream = sliceContextItems(upstreamState.included, limits.upstream);
735
+ const downstream = sliceContextItems(downstreamState.included, limits.downstream);
736
+ const linkedWorkflow = sliceContextItems(linkedWorkflowState.included, limits.linkedWorkflow);
737
+ const companionDocs = sliceContextItems(companionState.included, limits.companion);
738
+ const specs = sliceContextItems(specsState.included, limits.specs);
739
+ const summaryPoints = getSummaryPoints(item, limits.summaryPoints);
740
+ const acceptanceCriteria = getAcceptanceCriteria(item, limits.acceptanceCriteria);
741
+ const changedPaths = getRelevantChangedPaths(item, insights, safeOptions.changedPaths, limits.changedPaths);
742
+ const responseContract = buildResponseContract(taskKind, activeAgent && activeAgent.responseStyle);
743
+ const sessionHint = buildSessionHint(item, mode, taskKind, safeOptions.currentRoot, safeOptions.lastInjectedContext);
744
+ const profileLabel = profile.toUpperCase();
745
+ const modeLabel = mode === "summary-only" ? "SUMMARY" : mode === "diff-first" ? "DIFF-FIRST" : "STANDARD";
746
+ const excludedStaleCount =
747
+ upstreamState.staleExcluded +
748
+ downstreamState.staleExcluded +
749
+ linkedWorkflowState.staleExcluded +
750
+ companionState.staleExcluded +
751
+ specsState.staleExcluded;
752
+ const blockedDocCount =
753
+ upstreamState.blockedExcluded +
754
+ downstreamState.blockedExcluded +
755
+ linkedWorkflowState.blockedExcluded +
756
+ companionState.blockedExcluded +
757
+ specsState.blockedExcluded;
758
+
759
+ const openQuestions =
760
+ attentionReasons.length > 0
761
+ ? attentionReasons.map((reason) => `${reason.label}: ${reason.description}`)
762
+ : ["No explicit graph-risk question is currently detected for this item."];
763
+
764
+ const lines = [
765
+ "# Assistant Context Pack",
766
+ "",
767
+ `- Mode: ${modeLabel}`,
768
+ `- Profile: ${profileLabel}`,
769
+ `- Task type: ${taskKind}`,
770
+ activeAgent ? `- Active agent: ${activeAgent.displayName} (${activeAgent.id})` : "- Active agent: (none selected)",
771
+ "",
772
+ "## Current item",
773
+ `- ${describeContextItem(item)}`
774
+ ];
775
+
776
+ if (summaryPoints.length > 0) {
777
+ lines.push("", "## Summary", ...summaryPoints.map((entry) => `- ${entry}`));
778
+ }
779
+
780
+ if (acceptanceCriteria.length > 0) {
781
+ lines.push("", "## Acceptance criteria", ...acceptanceCriteria.map((entry) => `- ${entry}`));
782
+ }
783
+
784
+ if (changedPaths.length > 0) {
785
+ lines.push("", mode === "diff-first" ? "## Changed files first" : "## Recent changed files", ...changedPaths.map((entry) => `- ${entry}`));
786
+ }
787
+
788
+ const contextSections =
789
+ mode === "summary-only"
790
+ ? []
791
+ : getWorkflowStageRank(item.stage) >= 0
792
+ ? [
793
+ renderContextSection("Upstream", upstream),
794
+ renderContextSection("Downstream", downstream)
795
+ ]
796
+ : [renderContextSection("Linked workflow", linkedWorkflow)];
797
+
798
+ contextSections.forEach((section) => {
799
+ lines.push("", ...section);
800
+ });
801
+
802
+ if (mode !== "summary-only") {
803
+ lines.push("", ...renderContextSection("Companion docs", companionDocs));
804
+ lines.push("", ...renderContextSection("Specs", specs));
805
+ }
806
+ lines.push("", "## Open questions", ...openQuestions.map((entry) => `- ${entry}`));
807
+ lines.push("", "## Response contract", `- ${responseContract}`);
808
+
809
+ if (sessionHint) {
810
+ lines.push("", "## Session hygiene", `- ${sessionHint}`);
811
+ }
812
+
813
+ if (attentionReasons.length > 0) {
814
+ lines.push(
815
+ "",
816
+ "## Suggested next actions",
817
+ ...attentionReasons.map((reason) => {
818
+ const action = reason.remediation && reason.remediation.label ? reason.remediation.label : reason.label;
819
+ return `- ${action}`;
820
+ })
821
+ );
822
+ }
823
+
824
+ const text = lines.join("\n");
825
+ const relatedDocCount = 1 + upstream.length + downstream.length + linkedWorkflow.length + companionDocs.length + specs.length;
826
+ const lineCount = text.split("\n").length;
827
+ const charCount = text.length;
828
+ const tokenEstimate = estimateTokens(charCount);
829
+
830
+ return {
831
+ text,
832
+ summary: {
833
+ mode,
834
+ profile,
835
+ taskKind,
836
+ upstreamCount: upstream.length,
837
+ downstreamCount: downstream.length,
838
+ linkedWorkflowCount: linkedWorkflow.length,
839
+ companionCount: companionDocs.length,
840
+ specCount: specs.length,
841
+ changedPathCount: changedPaths.length,
842
+ summaryPointCount: summaryPoints.length,
843
+ acceptanceCriteriaCount: acceptanceCriteria.length,
844
+ docCount: relatedDocCount,
845
+ lineCount,
846
+ charCount,
847
+ tokenEstimate,
848
+ budgetLabel: classifyBudget(tokenEstimate),
849
+ excludedStaleCount,
850
+ blockedDocCount,
851
+ responseContract,
852
+ sessionHint,
853
+ trimmed:
854
+ upstreamState.included.length > upstream.length ||
855
+ downstreamState.included.length > downstream.length ||
856
+ linkedWorkflowState.included.length > linkedWorkflow.length ||
857
+ companionState.included.length > companionDocs.length ||
858
+ specsState.included.length > specs.length
859
+ },
860
+ attentionReasons,
861
+ sessionHygiene: sessionHint,
862
+ relatedChangedPaths: changedPaths
863
+ };
864
+ }
865
+
866
+ function buildDependencyMap(item, allItems) {
867
+ const insights = getRelationshipInsights(item, allItems);
868
+ const groups =
869
+ getWorkflowStageRank(item.stage) >= 0
870
+ ? [
871
+ { key: "upstream", label: "Upstream", items: insights.upstream.slice(0, 2) },
872
+ { key: "current", label: "Current", items: [item] },
873
+ { key: "downstream", label: "Downstream", items: insights.downstream.slice(0, 3) },
874
+ { key: "supporting", label: "Supporting docs", items: insights.supportingDocs.slice(0, 4) }
875
+ ]
876
+ : [
877
+ { key: "workflow", label: "Linked workflow", items: insights.linkedWorkflow.slice(0, 3) },
878
+ { key: "current", label: "Current", items: [item] },
879
+ { key: "supporting", label: "Supporting docs", items: insights.supportingDocs.slice(0, 4) }
880
+ ];
881
+
882
+ const nodes = groups.flatMap((group) => group.items);
883
+ const edges = dedupeItems(nodes)
884
+ .filter((candidate) => candidate && candidate.id !== item.id)
885
+ .map((candidate) => ({ from: item.id, to: candidate.id }));
886
+
887
+ return {
888
+ groups: groups.filter((group) => group.items.length > 0),
889
+ nodes: dedupeItems(nodes),
890
+ edges
891
+ };
892
+ }
893
+
894
+ window.CdxLogicsModel = {
895
+ buildContextPack,
896
+ buildDependencyMap,
897
+ collectCompanionDocs,
898
+ collectPrimaryFlowItems,
899
+ collectSpecs,
900
+ findManagedItemByReference,
901
+ getAttentionReasons,
902
+ getRelationshipInsights,
903
+ getStageHeading,
904
+ getStageLabel,
905
+ isCompanionStage,
906
+ isPrimaryFlowStage,
907
+ isRequestProcessed,
908
+ normalizeManagedDocValue
909
+ };
910
+ })();