noteconnection 1.7.0 → 1.8.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 (211) hide show
  1. package/LICENSE +674 -21
  2. package/README.md +242 -62
  3. package/dist/src/agent_workspace.contract.parity.test.js +475 -0
  4. package/dist/src/agent_workspace.frontend.test.js +9989 -0
  5. package/dist/src/agent_workspace.locale.contract.test.js +95 -0
  6. package/dist/src/agent_workspace.runtime.behavior.test.js +5072 -0
  7. package/dist/src/copy.assets.contract.test.js +130 -0
  8. package/dist/src/core/PathBridge.js +28 -3
  9. package/dist/src/export/WorkspaceExportBundle.js +1130 -0
  10. package/dist/src/export/WorkspaceExportBundle.test.js +1480 -0
  11. package/dist/src/export/index.js +18 -0
  12. package/dist/src/export/types.js +2 -0
  13. package/dist/src/fixrisk.issue.verifier.contract.test.js +3 -1
  14. package/dist/src/foundation.ann.runtime.contract.test.js +81 -0
  15. package/dist/src/foundation.release.evidence.contract.test.js +406 -0
  16. package/dist/src/foundation.sqlite.runtime.contract.test.js +82 -0
  17. package/dist/src/frontend/README.md +242 -62
  18. package/dist/src/frontend/agent_workspace.js +5353 -0
  19. package/dist/src/frontend/agent_workspace_runtime.js +4434 -0
  20. package/dist/src/frontend/analysis.js +31 -4
  21. package/dist/src/frontend/app.js +2178 -62
  22. package/dist/src/frontend/focus_mode_interactions.js +33 -0
  23. package/dist/src/frontend/godot_future_path_renderer.js +540 -0
  24. package/dist/src/frontend/godot_tree_interactions.js +178 -0
  25. package/dist/src/frontend/graph_state.mjs +105 -0
  26. package/dist/src/frontend/hosted_future_path_runtime.js +157 -0
  27. package/dist/src/frontend/i18n.mjs +186 -0
  28. package/dist/src/frontend/index.html +641 -266
  29. package/dist/src/frontend/layout_gpu.js +12 -4
  30. package/dist/src/frontend/locales/en.json +721 -5
  31. package/dist/src/frontend/locales/zh.json +721 -5
  32. package/dist/src/frontend/main.mjs +60 -0
  33. package/dist/src/frontend/markdown_runtime.js +827 -0
  34. package/dist/src/frontend/notemd.css +49 -0
  35. package/dist/src/frontend/notemd.html +7 -1
  36. package/dist/src/frontend/notemd.js +64 -0
  37. package/dist/src/frontend/path.html +107 -0
  38. package/dist/src/frontend/path_app.js +2108 -130
  39. package/dist/src/frontend/path_layout.mjs +143 -0
  40. package/dist/src/frontend/path_mermaid_utils.mjs +108 -0
  41. package/dist/src/frontend/path_modules_bridge.js +486 -0
  42. package/dist/src/frontend/path_state.mjs +118 -0
  43. package/dist/src/frontend/path_styles.css +146 -0
  44. package/dist/src/frontend/path_worker_bridge.mjs +85 -0
  45. package/dist/src/frontend/reader.js +393 -33
  46. package/dist/src/frontend/runtime_bridge.js +67 -54
  47. package/dist/src/frontend/runtime_bridge.mjs +279 -0
  48. package/dist/src/frontend/settings.js +130 -12
  49. package/dist/src/frontend/source_manager.js +190 -21
  50. package/dist/src/frontend/styles.css +2720 -72
  51. package/dist/src/frontend/workbench_state.mjs +101 -0
  52. package/dist/src/frontend/workspace_panes.js +10168 -0
  53. package/dist/src/frontend.locale.contract.test.js +62 -0
  54. package/dist/src/godot.sidecar.bootstrap.contract.test.js +244 -0
  55. package/dist/src/indexing/IndexLifecycle.js +195 -0
  56. package/dist/src/indexing/IndexLifecycle.test.js +49 -0
  57. package/dist/src/indexing/SegmentBuilder.js +64 -0
  58. package/dist/src/indexing/UnitBuilder.js +48 -0
  59. package/dist/src/indexing/types.js +2 -0
  60. package/dist/src/knowledge.api.contract.test.js +170 -0
  61. package/dist/src/learning/KnowledgeLearningPlatform.js +10386 -0
  62. package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +327 -0
  63. package/dist/src/learning/KnowledgeLearningPlatform.program-f.test.js +99 -0
  64. package/dist/src/learning/KnowledgeLearningPlatform.test.js +2971 -0
  65. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +2974 -0
  66. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +3928 -0
  67. package/dist/src/learning/answerReleaseReview.js +4319 -0
  68. package/dist/src/learning/answerReleaseReview.test.js +2888 -0
  69. package/dist/src/learning/api.js +2 -0
  70. package/dist/src/learning/conversationComposer.js +1480 -0
  71. package/dist/src/learning/conversationComposer.test.js +1817 -0
  72. package/dist/src/learning/domains/ConversationManager.js +53 -0
  73. package/dist/src/learning/domains/KnowledgeIngestor.js +238 -0
  74. package/dist/src/learning/domains/KnowledgeQuerier.js +187 -0
  75. package/dist/src/learning/domains/MasteryEngine.js +387 -0
  76. package/dist/src/learning/domains/MemoryPolicyManager.js +408 -0
  77. package/dist/src/learning/domains/QualityEvaluator.js +307 -0
  78. package/dist/src/learning/domains/TutorRouter.js +313 -0
  79. package/dist/src/learning/domains/index.js +33 -0
  80. package/dist/src/learning/domains/types.js +7 -0
  81. package/dist/src/learning/errors.js +29 -0
  82. package/dist/src/learning/evidenceContextAssembler.js +1176 -0
  83. package/dist/src/learning/evidenceContextAssembler.test.js +6332 -0
  84. package/dist/src/learning/graphContextAssembler.js +870 -0
  85. package/dist/src/learning/graphContextAssembler.test.js +1033 -0
  86. package/dist/src/learning/index.js +28 -0
  87. package/dist/src/learning/queryBackend.js +1898 -0
  88. package/dist/src/learning/queryBackend.test.js +955 -0
  89. package/dist/src/learning/ragContextPack.js +257 -0
  90. package/dist/src/learning/ragContextPack.test.js +160 -0
  91. package/dist/src/learning/ragPublicText.js +38 -0
  92. package/dist/src/learning/ragSufficiencyJudge.js +161 -0
  93. package/dist/src/learning/ragSufficiencyJudge.test.js +177 -0
  94. package/dist/src/learning/ragSufficiencyProviderJudge.js +227 -0
  95. package/dist/src/learning/ragSufficiencyProviderJudge.test.js +156 -0
  96. package/dist/src/learning/requestNormalization.js +198 -0
  97. package/dist/src/learning/runtimeCapability.js +4677 -0
  98. package/dist/src/learning/runtimeCapability.test.js +3635 -0
  99. package/dist/src/learning/store.js +1240 -0
  100. package/dist/src/learning/store.test.js +1126 -0
  101. package/dist/src/learning/tutorAdapter.js +2 -0
  102. package/dist/src/learning/types.js +2 -0
  103. package/dist/src/learning/vectorAccelerationAdapter.js +942 -0
  104. package/dist/src/learning/vectorAccelerationAdapter.test.js +382 -0
  105. package/dist/src/lfs.asset.policy.contract.test.js +153 -0
  106. package/dist/src/license.policy.contract.test.js +66 -0
  107. package/dist/src/memory/MemoryGovernance.js +74 -0
  108. package/dist/src/memory/MemoryGovernance.test.js +46 -0
  109. package/dist/src/memory/types.js +2 -0
  110. package/dist/src/mermaid.frontend.guard.contract.test.js +77 -0
  111. package/dist/src/middleware/auth.js +17 -0
  112. package/dist/src/middleware/body-parser.js +45 -0
  113. package/dist/src/middleware/cors.js +44 -0
  114. package/dist/src/middleware/index.js +21 -0
  115. package/dist/src/middleware/request-trace.js +96 -0
  116. package/dist/src/notemd/AppConfigToml.js +6 -4
  117. package/dist/src/notemd/MermaidProcessor.js +400 -50
  118. package/dist/src/notemd/NotemdService.js +498 -13
  119. package/dist/src/notemd/PromptManager.js +15 -0
  120. package/dist/src/notemd/cli/commands.js +357 -0
  121. package/dist/src/notemd/cli/dispatcher.js +225 -0
  122. package/dist/src/notemd/cli/index.js +169 -0
  123. package/dist/src/notemd/cli/parser.js +68 -0
  124. package/dist/src/notemd/cli/types.js +2 -0
  125. package/dist/src/notemd/constants.js +43 -0
  126. package/dist/src/notemd/diagram/diagramGenerationService.js +78 -0
  127. package/dist/src/notemd/diagram/diagramSpec.js +79 -0
  128. package/dist/src/notemd/diagram/diagramSpecResponseParser.js +131 -0
  129. package/dist/src/notemd/diagram/intent.js +95 -0
  130. package/dist/src/notemd/diagram/planner.js +71 -0
  131. package/dist/src/notemd/diagram/prompts/diagramSpecPrompt.js +42 -0
  132. package/dist/src/notemd/diagram/types.js +18 -0
  133. package/dist/src/notemd/index.js +26 -0
  134. package/dist/src/notemd/operations/capabilityManifest.js +23 -0
  135. package/dist/src/notemd/operations/cliContracts.js +17 -0
  136. package/dist/src/notemd/operations/configProfileCommands.js +85 -0
  137. package/dist/src/notemd/operations/registry.contract.test.js +95 -0
  138. package/dist/src/notemd/operations/registry.js +991 -0
  139. package/dist/src/notemd/operations/types.js +2 -0
  140. package/dist/src/notemd/providerDiagnostics.js +220 -0
  141. package/dist/src/notemd/providerProfiles.js +42 -0
  142. package/dist/src/notemd/providerTemplates.js +231 -0
  143. package/dist/src/notemd/search/DuckDuckGoProvider.js +39 -0
  144. package/dist/src/notemd/search/SearchManager.js +13 -0
  145. package/dist/src/notemd/search/SearchProvider.js +2 -0
  146. package/dist/src/notemd/search/TavilyProvider.js +44 -0
  147. package/dist/src/notemd.agent.manifest.test.js +85 -0
  148. package/dist/src/notemd.api.contract.test.js +14 -1
  149. package/dist/src/notemd.app_config_toml.test.js +2 -1
  150. package/dist/src/notemd.batch.workflow.test.js +117 -0
  151. package/dist/src/notemd.cli.e2e.test.js +136 -0
  152. package/dist/src/notemd.core.test.js +51 -0
  153. package/dist/src/notemd.diagram.pipeline.test.js +233 -0
  154. package/dist/src/notemd.providerTemplates.test.js +34 -0
  155. package/dist/src/notemd.server.integration.test.js +143 -35
  156. package/dist/src/notemd.workflow.pipeline.test.js +162 -0
  157. package/dist/src/pathbridge.handshake.contract.test.js +16 -2
  158. package/dist/src/pathmode.background.contract.test.js +69 -0
  159. package/dist/src/pathmode.settings.api.contract.test.js +9 -0
  160. package/dist/src/pkg.sidecar.contract.test.js +9 -3
  161. package/dist/src/platform/ExportProfile.js +58 -0
  162. package/dist/src/platform/PlatformCapabilities.js +45 -0
  163. package/dist/src/platform/PlatformCapabilities.test.js +30 -0
  164. package/dist/src/platform/RenderMaterializer.js +33 -0
  165. package/dist/src/platform/RenderMaterializer.test.js +32 -0
  166. package/dist/src/query_backend.external_http.integration.test.js +410 -0
  167. package/dist/src/reader_renderer.js +404 -3
  168. package/dist/src/reader_renderer.test.js +87 -0
  169. package/dist/src/release.godot.mirror.contract.test.js +73 -0
  170. package/dist/src/resources/ResourceRegistry.js +223 -0
  171. package/dist/src/resources/ResourceRegistry.test.js +61 -0
  172. package/dist/src/resources/types.js +2 -0
  173. package/dist/src/routes/agentWorkspaceDiagnostics.js +173 -0
  174. package/dist/src/routes/data.js +267 -0
  175. package/dist/src/routes/diagnostics.js +51 -0
  176. package/dist/src/routes/index.js +23 -0
  177. package/dist/src/routes/knowledge.js +968 -0
  178. package/dist/src/routes/markdown.js +287 -0
  179. package/dist/src/routes/notemd.js +565 -0
  180. package/dist/src/routes/registry.contract.test.js +130 -0
  181. package/dist/src/routes/render.js +285 -0
  182. package/dist/src/routes/runtimeRunbookRouteOps.js +149 -0
  183. package/dist/src/routes/runtimeRunbookRouteOps.test.js +194 -0
  184. package/dist/src/routes/settings.js +6 -0
  185. package/dist/src/routes/staticFiles.js +94 -0
  186. package/dist/src/routes/types.js +2 -0
  187. package/dist/src/runtime.transport.adapter.contract.test.js +81 -0
  188. package/dist/src/server.js +11090 -1549
  189. package/dist/src/server.migration.test.js +193 -21
  190. package/dist/src/server.port.fallback.contract.test.js +63 -0
  191. package/dist/src/session/SessionStateStore.js +81 -0
  192. package/dist/src/session/SessionStateStore.test.js +58 -0
  193. package/dist/src/session/types.js +2 -0
  194. package/dist/src/settings.runtime.contract.test.js +50 -0
  195. package/dist/src/shared/types.contract.test.js +107 -0
  196. package/dist/src/shared/types.js +22 -0
  197. package/dist/src/sidecar.replacement.boundary.contract.test.js +128 -0
  198. package/dist/src/sidecar.supply.readiness.contract.test.js +144 -0
  199. package/dist/src/source_manager.loadflow.test.js +46 -0
  200. package/dist/src/startup.layout.snapshot.contract.test.js +57 -0
  201. package/dist/src/tauri.frontend.build.contract.test.js +60 -0
  202. package/dist/src/tauri.sidecar.cleanup.contract.test.js +21 -0
  203. package/dist/src/utils/RuntimePaths.js +4 -13
  204. package/dist/src/utils/platform.js +153 -0
  205. package/dist/src/workflows/WorkflowArtifactStore.js +96 -0
  206. package/dist/src/workflows/WorkflowArtifactStore.test.js +80 -0
  207. package/dist/src/workflows/types.js +2 -0
  208. package/dist/src/workspace/WorkspaceRegistry.js +122 -0
  209. package/dist/src/workspace/WorkspaceRegistry.test.js +29 -0
  210. package/dist/src/workspace/types.js +2 -0
  211. package/package.json +53 -10
@@ -1,8 +1,25 @@
1
1
  /**
2
2
  * Path Mode Application Controller
3
3
  * Handles interaction, rendering, and worker communication.
4
+ *
5
+ * Phase 4 P5: Canonical utility implementations now live in
6
+ * path_mermaid_utils.mjs and path_state.mjs, exposed via
7
+ * window.pathModules (loaded by path_modules_bridge.js).
8
+ * The _methodName functions below delegate to window.pathModules
9
+ * when available, falling back to inline implementations.
4
10
  */
5
11
 
12
+ (function () {
13
+ 'use strict';
14
+ var _pm = window.pathModules;
15
+ if (_pm && _pm.utils) {
16
+ // Pre-bind canonical implementations before pathApp definition.
17
+ // These will be used by the methods defined below.
18
+ window._pathUtils = _pm.utils;
19
+ window._pathState = _pm.state;
20
+ }
21
+ })();
22
+
6
23
  window.pathApp = {
7
24
  canvas: null,
8
25
  ctx: null,
@@ -39,6 +56,29 @@ window.pathApp = {
39
56
  pendingWindowVisibility: null,
40
57
  semanticA11yLastSummaryKey: '',
41
58
  semanticA11yLastAnnouncementAt: 0,
59
+ semanticA11yDeferredRefreshTimer: null,
60
+ learningWorkbench: {
61
+ userId: 'path_user_default',
62
+ loading: false,
63
+ lastError: '',
64
+ lastUpdatedAt: '',
65
+ sessionPlan: null,
66
+ qualitySnapshot: null,
67
+ qualityGateEvaluation: null,
68
+ qualityBaselineSnapshot: null,
69
+ misconceptions: null,
70
+ runtimeState: null,
71
+ tutorFeedback: null,
72
+ sessionExecution: null,
73
+ sessionHistory: null,
74
+ sessionHistoryQuery: {
75
+ limit: 8,
76
+ offset: 0,
77
+ executionKind: 'all',
78
+ fromDate: '',
79
+ toDate: '',
80
+ },
81
+ },
42
82
 
43
83
  // Animation State
44
84
  animationId: null,
@@ -323,6 +363,31 @@ window.pathApp = {
323
363
  return true;
324
364
  },
325
365
 
366
+ _isDetachedPreviewWithoutRuntimeBootstrap: function() {
367
+ if (typeof window === 'undefined' || window.__TAURI__) {
368
+ return false;
369
+ }
370
+ const hasDetachedPreviewScript = Array.from(document.scripts || []).some((script) =>
371
+ /__tauri_cli/.test(String(script && (script.text || script.textContent) || ''))
372
+ );
373
+ if (!hasDetachedPreviewScript) {
374
+ return false;
375
+ }
376
+ if (!window.NoteConnectionRuntime || typeof window.NoteConnectionRuntime.getBaseUrl !== 'function') {
377
+ return true;
378
+ }
379
+ const currentOrigin = String((window.location && window.location.origin) || '').trim().replace(/\/+$/, '');
380
+ const baseUrl = String(window.NoteConnectionRuntime.getBaseUrl() || '').trim();
381
+ if (!currentOrigin || !baseUrl) {
382
+ return true;
383
+ }
384
+ try {
385
+ return currentOrigin !== new URL(baseUrl).origin;
386
+ } catch (_error) {
387
+ return true;
388
+ }
389
+ },
390
+
326
391
  _getBridgeWsUrl: function() {
327
392
  if (typeof window !== 'undefined' && window.NoteConnectionRuntime && typeof window.NoteConnectionRuntime.getBridgeWsUrl === 'function') {
328
393
  return window.NoteConnectionRuntime.getBridgeWsUrl('frontend');
@@ -598,6 +663,313 @@ window.pathApp = {
598
663
  };
599
664
  },
600
665
 
666
+ _getReaderLikeMermaidConfig: function(theme = 'dark') {
667
+ return {
668
+ startOnLoad: false,
669
+ theme,
670
+ securityLevel: 'loose',
671
+ htmlLabels: true,
672
+ };
673
+ },
674
+
675
+ _getBridgeTextMeasureContext: function() {
676
+ if (!this._bridgeTextMeasureCanvas) {
677
+ this._bridgeTextMeasureCanvas = document.createElement('canvas');
678
+ }
679
+ if (!this._bridgeTextMeasureContext) {
680
+ this._bridgeTextMeasureContext = this._bridgeTextMeasureCanvas.getContext('2d');
681
+ }
682
+ return this._bridgeTextMeasureContext;
683
+ },
684
+
685
+ _measureBridgeCanvasTextWidth: function(text, fontDescriptor) {
686
+ const context = this._getBridgeTextMeasureContext();
687
+ if (!context) {
688
+ return this._estimateBridgeTextLineWidth(text, fontDescriptor.fontSize || 16);
689
+ }
690
+ context.font = [
691
+ fontDescriptor.fontStyle || 'normal',
692
+ fontDescriptor.fontWeight || '400',
693
+ `${fontDescriptor.fontSize || 16}px`,
694
+ fontDescriptor.fontFamily || '"trebuchet ms", verdana, arial, sans-serif',
695
+ ].join(' ');
696
+ return context.measureText(String(text || '')).width;
697
+ },
698
+
699
+ _extractBridgeForeignObjectSourceLines: function(foreignObject) {
700
+ const htmlRoot = foreignObject && (
701
+ foreignObject.querySelector('div, span, p')
702
+ || foreignObject.firstElementChild
703
+ || foreignObject
704
+ );
705
+ if (!htmlRoot) {
706
+ return { lines: [], fromRenderedLayout: false };
707
+ }
708
+
709
+ const collectNormalizedLines = (value) => String(value || '')
710
+ .split(/\r?\n/)
711
+ .map((line) => this._normalizeBridgeInlineText(line))
712
+ .filter(Boolean);
713
+
714
+ // Prefer the browser's actual rendered line layout so the bridge mirrors
715
+ // the reader's foreignObject wrapping instead of guessing it again.
716
+ const renderedText = typeof htmlRoot.innerText === 'string' ? htmlRoot.innerText : '';
717
+ const renderedLines = collectNormalizedLines(renderedText);
718
+ if (renderedLines.length > 0) {
719
+ return {
720
+ lines: renderedLines,
721
+ fromRenderedLayout: true,
722
+ };
723
+ }
724
+
725
+ const paragraphs = Array.from(htmlRoot.querySelectorAll('p'));
726
+ const htmlSegments = paragraphs.length > 0
727
+ ? paragraphs.map((paragraph) => String(paragraph.innerHTML || ''))
728
+ : [String(htmlRoot.innerHTML || htmlRoot.textContent || '')];
729
+ const scratch = document.createElement('div');
730
+ const lines = [];
731
+ htmlSegments.forEach((segment) => {
732
+ scratch.innerHTML = String(segment || '').replace(/<br\s*\/?>/gi, '\n');
733
+ collectNormalizedLines(scratch.innerText || scratch.textContent || '')
734
+ .forEach((line) => lines.push(line));
735
+ });
736
+ return {
737
+ lines,
738
+ fromRenderedLayout: false,
739
+ };
740
+ },
741
+
742
+ _wrapBridgeForeignObjectLines: function(lines, fontDescriptor, maxWidth, whiteSpaceMode) {
743
+ const safeLines = Array.isArray(lines) ? lines.filter((line) => String(line || '').trim()) : [];
744
+ if (safeLines.length === 0) {
745
+ return [];
746
+ }
747
+ if (String(whiteSpaceMode || '').toLowerCase() === 'nowrap') {
748
+ return safeLines;
749
+ }
750
+
751
+ const wrapped = [];
752
+ const hardMaxLines = 24;
753
+ const measure = (text) => this._measureBridgeCanvasTextWidth(text, fontDescriptor);
754
+
755
+ safeLines.forEach((line) => {
756
+ const normalized = this._normalizeBridgeInlineText(line);
757
+ if (!normalized) {
758
+ return;
759
+ }
760
+ if (measure(normalized) <= maxWidth) {
761
+ wrapped.push(normalized);
762
+ return;
763
+ }
764
+
765
+ const tokens = /\s/.test(normalized)
766
+ ? normalized.split(/\s+/).filter(Boolean)
767
+ : Array.from(normalized);
768
+ let current = '';
769
+ const pushCurrent = () => {
770
+ if (current) {
771
+ wrapped.push(current);
772
+ current = '';
773
+ }
774
+ };
775
+
776
+ for (const token of tokens) {
777
+ const candidate = !current
778
+ ? token
779
+ : (/\s/.test(normalized) ? `${current} ${token}` : `${current}${token}`);
780
+ if (!current || measure(candidate) <= maxWidth) {
781
+ current = candidate;
782
+ continue;
783
+ }
784
+ pushCurrent();
785
+ if (measure(token) <= maxWidth) {
786
+ current = token;
787
+ continue;
788
+ }
789
+ let fragment = '';
790
+ for (const char of Array.from(token)) {
791
+ const fragmentCandidate = fragment + char;
792
+ if (!fragment || measure(fragmentCandidate) <= maxWidth) {
793
+ fragment = fragmentCandidate;
794
+ continue;
795
+ }
796
+ wrapped.push(fragment);
797
+ fragment = char;
798
+ if (wrapped.length >= hardMaxLines) {
799
+ break;
800
+ }
801
+ }
802
+ if (fragment && wrapped.length < hardMaxLines) {
803
+ current = fragment;
804
+ }
805
+ if (wrapped.length >= hardMaxLines) {
806
+ break;
807
+ }
808
+ }
809
+ pushCurrent();
810
+ });
811
+
812
+ return wrapped.slice(0, hardMaxLines);
813
+ },
814
+
815
+ _convertBridgeForeignObjectToSvgText: function(svgElement, foreignObject) {
816
+ if (!foreignObject || !foreignObject.parentNode) {
817
+ return;
818
+ }
819
+
820
+ const extracted = this._extractBridgeForeignObjectSourceLines(foreignObject);
821
+ if (!extracted || extracted.lines.length === 0) {
822
+ foreignObject.remove();
823
+ return;
824
+ }
825
+
826
+ const styleNode = foreignObject.querySelector('.nodeLabel, .edgeLabel, span, div, p') || foreignObject.firstElementChild || foreignObject;
827
+ const computedStyle = window.getComputedStyle(styleNode);
828
+ const fontSize = Number.parseFloat(computedStyle.fontSize || '') || 16;
829
+ const fontFamily = computedStyle.fontFamily || '"trebuchet ms", verdana, arial, sans-serif';
830
+ const fontWeight = computedStyle.fontWeight || '400';
831
+ const fontStyle = computedStyle.fontStyle || 'normal';
832
+ const lineHeight = Number.parseFloat(computedStyle.lineHeight || '') || (fontSize * 1.5);
833
+ const textAlign = String(computedStyle.textAlign || '').trim().toLowerCase();
834
+ const whiteSpace = String(computedStyle.whiteSpace || '').trim().toLowerCase();
835
+ const fill = computedStyle.color || '#ccc';
836
+
837
+ const x = this._parseBridgeNumericAttribute(foreignObject, 'x', 0);
838
+ const y = this._parseBridgeNumericAttribute(foreignObject, 'y', 0);
839
+ const width = this._parseBridgeNumericAttribute(foreignObject, 'width', 0);
840
+ const height = this._parseBridgeNumericAttribute(foreignObject, 'height', 0);
841
+ const horizontalPadding = 6;
842
+ const verticalPadding = 4;
843
+ const usableWidth = Math.max(12, width - horizontalPadding * 2);
844
+ const fontDescriptor = { fontSize, fontFamily, fontWeight, fontStyle };
845
+ const wrappedLines = extracted.fromRenderedLayout
846
+ ? extracted.lines.slice(0, 24)
847
+ : this._wrapBridgeForeignObjectLines(extracted.lines, fontDescriptor, usableWidth, whiteSpace);
848
+ if (wrappedLines.length === 0) {
849
+ foreignObject.remove();
850
+ return;
851
+ }
852
+
853
+ const totalHeight = (wrappedLines.length - 1) * lineHeight + fontSize;
854
+ const startY = y + Math.max(verticalPadding + fontSize * 0.85, (height - totalHeight) * 0.5 + fontSize * 0.85);
855
+ let anchor = 'middle';
856
+ let textX = x + (width * 0.5);
857
+ if (textAlign === 'left' || textAlign === 'start') {
858
+ anchor = 'start';
859
+ textX = x + horizontalPadding;
860
+ } else if (textAlign === 'right' || textAlign === 'end') {
861
+ anchor = 'end';
862
+ textX = x + width - horizontalPadding;
863
+ }
864
+
865
+ const textElement = document.createElementNS('http://www.w3.org/2000/svg', 'text');
866
+ textElement.setAttribute('fill', fill);
867
+ textElement.setAttribute('font-size', `${fontSize}`);
868
+ textElement.setAttribute('font-family', fontFamily);
869
+ textElement.setAttribute('font-weight', fontWeight);
870
+ textElement.setAttribute('font-style', fontStyle);
871
+ textElement.setAttribute('text-anchor', anchor);
872
+ textElement.setAttribute('dominant-baseline', 'alphabetic');
873
+
874
+ wrappedLines.forEach((line, index) => {
875
+ const tspan = document.createElementNS('http://www.w3.org/2000/svg', 'tspan');
876
+ tspan.textContent = line;
877
+ tspan.setAttribute('x', `${textX}`);
878
+ if (index === 0) {
879
+ tspan.setAttribute('y', `${startY}`);
880
+ } else {
881
+ tspan.setAttribute('dy', `${lineHeight}`);
882
+ }
883
+ textElement.appendChild(tspan);
884
+ });
885
+
886
+ foreignObject.parentNode.insertBefore(textElement, foreignObject);
887
+ foreignObject.remove();
888
+ },
889
+
890
+ _convertBridgeForeignObjectsToSvgText: function(svgElement) {
891
+ const foreignObjects = Array.from(svgElement.querySelectorAll('foreignObject'));
892
+ foreignObjects.forEach((foreignObject) => this._convertBridgeForeignObjectToSvgText(svgElement, foreignObject));
893
+ },
894
+
895
+ _renderParityMermaidSvg: async function(sourceText, options = {}) {
896
+ if (!window.mermaid) {
897
+ throw new Error('Mermaid runtime is unavailable in the frontend renderer.');
898
+ }
899
+
900
+ const source = this._normalizeBridgeMermaidDefinition(sourceText || '');
901
+ if (!source) {
902
+ throw new Error('Mermaid render request is missing source.');
903
+ }
904
+
905
+ const theme = String(options.theme || 'dark') === 'default' ? 'default' : 'dark';
906
+ window.mermaid.initialize(this._getReaderLikeMermaidConfig(theme));
907
+
908
+ const requestedWidth = Number.isFinite(Number(options.maxWidth)) && Number(options.maxWidth) > 0
909
+ ? Math.floor(Number(options.maxWidth))
910
+ : 620;
911
+ const requestedHeight = Number.isFinite(Number(options.maxHeight)) && Number(options.maxHeight) > 0
912
+ ? Math.floor(Number(options.maxHeight))
913
+ : 860;
914
+ const hostWidth = Math.max(240, requestedWidth);
915
+ const host = document.createElement('div');
916
+ host.className = 'mermaid-render-host-offscreen';
917
+ host.style.position = 'fixed';
918
+ host.style.left = '-20000px';
919
+ host.style.top = '0';
920
+ host.style.width = String(hostWidth) + 'px';
921
+ host.style.minWidth = String(hostWidth) + 'px';
922
+ host.style.height = 'auto';
923
+ host.style.overflow = 'visible';
924
+ host.style.opacity = '0';
925
+ host.style.pointerEvents = 'none';
926
+ host.style.background = 'transparent';
927
+ host.style.fontFamily = '"Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", "Segoe UI", sans-serif';
928
+ document.body.appendChild(host);
929
+
930
+ try {
931
+ const renderId = 'parity-mermaid-' + Date.now() + '-' + Math.random().toString(16).slice(2);
932
+ const result = await window.mermaid.render(renderId, source, host);
933
+ if (result && typeof result.svg === 'string' && this._isBridgeMermaidErrorSvgMarkup(result.svg)) {
934
+ throw new Error('Frontend Mermaid parity renderer returned an error SVG instead of a diagram.');
935
+ }
936
+ let svgElement = host.querySelector('svg');
937
+ if (!svgElement) {
938
+ const parser = new DOMParser();
939
+ const documentSvg = parser.parseFromString(result.svg, 'image/svg+xml');
940
+ const parsedSvg = documentSvg.querySelector('svg');
941
+ if (!parsedSvg) {
942
+ throw new Error('Frontend Mermaid parity renderer did not produce an SVG root.');
943
+ }
944
+ host.replaceChildren(parsedSvg);
945
+ svgElement = host.querySelector('svg');
946
+ }
947
+ if (!svgElement) {
948
+ throw new Error('Frontend Mermaid parity renderer did not produce an SVG root.');
949
+ }
950
+
951
+ svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
952
+ svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
953
+ this._convertBridgeForeignObjectsToSvgText(svgElement);
954
+ const naturalSize = this._tightenBridgeMermaidSvgBounds(svgElement);
955
+ const clampedSize = this._clampBridgeMermaidSize(naturalSize.width, naturalSize.height, requestedWidth, requestedHeight);
956
+ svgElement.setAttribute('width', String(clampedSize.width));
957
+ svgElement.setAttribute('height', String(clampedSize.height));
958
+ svgElement.style.maxWidth = '100%';
959
+ svgElement.style.height = 'auto';
960
+ svgElement.style.background = 'transparent';
961
+
962
+ return {
963
+ source,
964
+ svg: this._serializeBridgeMermaidSvg(svgElement),
965
+ width: Math.max(1, Math.round(clampedSize.width)),
966
+ height: Math.max(1, Math.round(clampedSize.height))
967
+ };
968
+ } finally {
969
+ host.remove();
970
+ }
971
+ },
972
+
601
973
  _upsertBridgeMermaidOverrideStyle: function(svgElement) {
602
974
  const styleId = 'noteconnection-mermaid-overrides';
603
975
  let styleNode = svgElement.querySelector('#' + styleId);
@@ -725,15 +1097,40 @@ window.pathApp = {
725
1097
  },
726
1098
 
727
1099
  _normalizeBridgeInlineText: function(text) {
1100
+ if (window._pathUtils) return window._pathUtils.normalizeBridgeInlineText(text);
728
1101
  return String(text || '').replace(/\s+/g, ' ').trim();
729
1102
  },
730
1103
 
1104
+ _normalizeBridgeMermaidDefinition: function(source) {
1105
+ if (window._pathUtils && typeof window._pathUtils.normalizeBridgeMermaidDefinition === 'function') {
1106
+ return window._pathUtils.normalizeBridgeMermaidDefinition(source);
1107
+ }
1108
+ return String(source || '').trim();
1109
+ },
1110
+
1111
+ _isBridgeMermaidErrorSvgMarkup: function(markup) {
1112
+ const text = String(markup || '').toLowerCase();
1113
+ if (!text) {
1114
+ return false;
1115
+ }
1116
+ return (
1117
+ text.includes('syntax error in text') ||
1118
+ text.includes('lexical error on line') ||
1119
+ text.includes('parse error on line') ||
1120
+ text.includes('mermaid version') ||
1121
+ text.includes('class="error-icon"') ||
1122
+ text.includes('id="error-icon"')
1123
+ );
1124
+ },
1125
+
731
1126
  _parseBridgeNumericAttribute: function(element, name, fallback = 0) {
1127
+ if (window._pathUtils) return window._pathUtils.parseBridgeNumericAttribute(element, name, fallback);
732
1128
  const numeric = Number.parseFloat(String(element?.getAttribute?.(name) || ''));
733
1129
  return Number.isFinite(numeric) ? numeric : fallback;
734
1130
  },
735
1131
 
736
1132
  _extractBridgeInlineStyleValue: function(styleValue, propertyName) {
1133
+ if (window._pathUtils) return window._pathUtils.extractBridgeInlineStyleValue(styleValue, propertyName);
737
1134
  if (!styleValue) {
738
1135
  return null;
739
1136
  }
@@ -743,6 +1140,7 @@ window.pathApp = {
743
1140
  },
744
1141
 
745
1142
  _resolveBridgeTextProperty: function(element, propertyName) {
1143
+ if (window._pathUtils) return window._pathUtils.resolveBridgeTextProperty(element, propertyName);
746
1144
  let current = element;
747
1145
  while (current) {
748
1146
  const attributeValue = current.getAttribute && current.getAttribute(propertyName);
@@ -759,6 +1157,7 @@ window.pathApp = {
759
1157
  },
760
1158
 
761
1159
  _parseBridgeCssLength: function(lengthValue, baseFontSize) {
1160
+ if (window._pathUtils) return window._pathUtils.parseBridgeCssLength(lengthValue, baseFontSize);
762
1161
  if (!lengthValue) {
763
1162
  return 0;
764
1163
  }
@@ -780,12 +1179,14 @@ window.pathApp = {
780
1179
  },
781
1180
 
782
1181
  _resolveBridgeSvgFontSize: function(element) {
1182
+ if (window._pathUtils) return window._pathUtils.resolveBridgeSvgFontSize(element);
783
1183
  const resolvedValue = this._resolveBridgeTextProperty(element, 'font-size');
784
1184
  const parsed = this._parseBridgeCssLength(resolvedValue, 16);
785
1185
  return parsed > 0 ? parsed : 16;
786
1186
  },
787
1187
 
788
1188
  _resolveBridgeSvgLineHeight: function(element, fontSize) {
1189
+ if (window._pathUtils) return window._pathUtils.resolveBridgeSvgLineHeight(element, fontSize);
789
1190
  const resolvedValue = this._resolveBridgeTextProperty(element, 'line-height');
790
1191
  const parsed = this._parseBridgeCssLength(resolvedValue, fontSize);
791
1192
  return parsed > 0 ? parsed : Math.max(fontSize * 1.18, fontSize + 4);
@@ -796,6 +1197,7 @@ window.pathApp = {
796
1197
  },
797
1198
 
798
1199
  _estimateBridgeGlyphWidthUnits: function(char) {
1200
+ if (window._pathUtils) return window._pathUtils.estimateBridgeGlyphWidthUnits(char);
799
1201
  if (!char) {
800
1202
  return 0;
801
1203
  }
@@ -827,6 +1229,7 @@ window.pathApp = {
827
1229
  },
828
1230
 
829
1231
  _estimateBridgeTextLineWidth: function(text, fontSize) {
1232
+ if (window._pathUtils) return window._pathUtils.estimateBridgeTextLineWidth(text, fontSize);
830
1233
  let units = 0;
831
1234
  for (const char of Array.from(String(text || ''))) {
832
1235
  units += this._estimateBridgeGlyphWidthUnits(char);
@@ -835,6 +1238,7 @@ window.pathApp = {
835
1238
  },
836
1239
 
837
1240
  _splitBridgeTokenForWrap: function(token, fontSize, maxLineWidth) {
1241
+ if (window._pathUtils) return window._pathUtils.splitBridgeTokenForWrap(token, fontSize, maxLineWidth);
838
1242
  const wrapped = [];
839
1243
  let segment = '';
840
1244
  for (const char of Array.from(token)) {
@@ -853,6 +1257,7 @@ window.pathApp = {
853
1257
  },
854
1258
 
855
1259
  _wrapBridgeMeasurementLine: function(line, fontSize, maxLineWidth) {
1260
+ if (window._pathUtils) return window._pathUtils.wrapBridgeMeasurementLine(line, fontSize, maxLineWidth);
856
1261
  const normalizedLine = this._normalizeBridgeInlineText(line);
857
1262
  if (!normalizedLine) {
858
1263
  return [];
@@ -1308,79 +1713,38 @@ window.pathApp = {
1308
1713
  },
1309
1714
 
1310
1715
  _renderMermaidForBridge: async function(payload) {
1311
- if (!window.mermaid) {
1312
- throw new Error('Mermaid runtime is unavailable in the frontend renderer.');
1313
- }
1314
-
1315
1716
  const requestId = String(payload?.requestId || '').trim();
1316
- const source = String(payload?.source || '').trim();
1317
- if (!requestId || !source) {
1717
+ if (!requestId) {
1318
1718
  throw new Error('Mermaid render request is missing a request id or source.');
1319
1719
  }
1320
1720
 
1321
1721
  const includeStages = payload?.includeStages === true;
1322
1722
  const includeSvg = includeStages || payload?.includeSvg === true;
1323
1723
  const stageSnapshots = [];
1324
- const theme = String(payload?.theme || 'dark') === 'default' ? 'default' : 'dark';
1325
- window.mermaid.initialize(this._getBridgeMermaidConfig(theme));
1326
-
1327
- const requestedWidth = Number.isFinite(Number(payload?.maxWidth)) && Number(payload.maxWidth) > 0 ? Math.floor(Number(payload.maxWidth)) : 1600;
1328
- const hostWidth = Math.max(480, requestedWidth);
1329
- const host = document.createElement('div');
1330
- host.style.position = 'fixed';
1331
- host.style.left = '-20000px';
1332
- host.style.top = '0';
1333
- host.style.width = String(hostWidth) + 'px';
1334
- host.style.minWidth = String(hostWidth) + 'px';
1335
- host.style.height = 'auto';
1336
- host.style.overflow = 'visible';
1337
- host.style.opacity = '0';
1338
- host.style.pointerEvents = 'none';
1339
- host.style.background = 'transparent';
1340
- host.style.fontFamily = '"Microsoft YaHei UI", "Microsoft YaHei", "PingFang SC", "Noto Sans CJK SC", "Segoe UI", sans-serif';
1341
- document.body.appendChild(host);
1342
1724
 
1343
1725
  let objectUrl = null;
1344
1726
  try {
1345
- const renderId = 'bridge-mermaid-' + Date.now() + '-' + Math.random().toString(16).slice(2);
1346
- const result = await window.mermaid.render(renderId, source, host);
1347
- let svgElement = host.querySelector('svg');
1348
- if (!svgElement) {
1349
- const parser = new DOMParser();
1350
- const documentSvg = parser.parseFromString(result.svg, 'image/svg+xml');
1351
- const parsedSvg = documentSvg.querySelector('svg');
1352
- if (!parsedSvg) {
1353
- throw new Error('Frontend Mermaid renderer did not produce an SVG root.');
1354
- }
1355
- host.replaceChildren(parsedSvg);
1356
- svgElement = host.querySelector('svg');
1357
- }
1727
+ const parity = await this._renderParityMermaidSvg(payload?.source || '', {
1728
+ theme: payload?.theme || 'dark',
1729
+ maxWidth: payload?.maxWidth,
1730
+ maxHeight: payload?.maxHeight
1731
+ });
1732
+ const parser = new DOMParser();
1733
+ const documentSvg = parser.parseFromString(parity.svg, 'image/svg+xml');
1734
+ const svgElement = documentSvg.querySelector('svg');
1358
1735
  if (!svgElement) {
1359
- throw new Error('Frontend Mermaid renderer did not produce an SVG root.');
1736
+ throw new Error('Frontend Mermaid parity renderer did not produce an SVG root.');
1360
1737
  }
1361
-
1362
- svgElement.setAttribute('xmlns', 'http://www.w3.org/2000/svg');
1363
- svgElement.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink');
1364
1738
  if (includeStages) {
1365
1739
  stageSnapshots.push(this._captureBridgeMermaidStage('raw', svgElement));
1366
- }
1367
-
1368
- this._normalizeBridgeMermaidSvg(svgElement);
1369
- if (includeStages) {
1370
1740
  stageSnapshots.push(this._captureBridgeMermaidStage('visual_normalized', svgElement));
1371
- }
1372
-
1373
- this._fitBridgeMermaidLabelShapes(svgElement);
1374
- if (includeStages) {
1375
1741
  stageSnapshots.push(this._captureBridgeMermaidStage('labels_fitted', svgElement));
1376
1742
  }
1377
1743
 
1378
- const naturalSize = this._tightenBridgeMermaidSvgBounds(svgElement);
1379
- const clampedSize = this._clampBridgeMermaidSize(naturalSize.width, naturalSize.height, payload?.maxWidth, payload?.maxHeight);
1380
1744
  const requestedRenderScale = Number.isFinite(Number(payload?.renderScale)) && Number(payload.renderScale) > 0 ? Number(payload.renderScale) : 1;
1381
1745
  const rasterScale = Math.min(4, Math.max(1, requestedRenderScale));
1382
- const rasterWidth = Math.max(1, Math.round(clampedSize.width * rasterScale));
1383
- const rasterHeight = Math.max(1, Math.round(clampedSize.height * rasterScale));
1746
+ const rasterWidth = Math.max(1, Math.round(parity.width * rasterScale));
1747
+ const rasterHeight = Math.max(1, Math.round(parity.height * rasterScale));
1384
1748
  svgElement.setAttribute('width', String(rasterWidth));
1385
1749
  svgElement.setAttribute('height', String(rasterHeight));
1386
1750
  svgElement.style.maxWidth = String(rasterWidth) + 'px';
@@ -1431,7 +1795,6 @@ window.pathApp = {
1431
1795
  if (objectUrl) {
1432
1796
  URL.revokeObjectURL(objectUrl);
1433
1797
  }
1434
- host.remove();
1435
1798
  }
1436
1799
  },
1437
1800
 
@@ -1546,98 +1909,1520 @@ window.pathApp = {
1546
1909
  });
1547
1910
  },
1548
1911
 
1549
- _sanitizeTargetIds: function(candidateIds) {
1550
- if (!Array.isArray(candidateIds)) {
1551
- return [];
1912
+ _sanitizeTargetIds: function(candidateIds) {
1913
+ if (!Array.isArray(candidateIds)) {
1914
+ return [];
1915
+ }
1916
+ const uniqueIds = [];
1917
+ const seen = new Set();
1918
+ candidateIds.forEach((id) => {
1919
+ if (typeof id !== 'string') return;
1920
+ const normalized = id.trim();
1921
+ if (!normalized || seen.has(normalized)) return;
1922
+ seen.add(normalized);
1923
+ uniqueIds.push(normalized);
1924
+ });
1925
+ return uniqueIds;
1926
+ },
1927
+
1928
+ _getDefaultTargetIds: function(limit = 1) {
1929
+ const maxCount = Math.max(0, Number.isFinite(limit) ? Math.floor(limit) : 0);
1930
+ if (maxCount <= 0) return [];
1931
+ const result = [];
1932
+ const seen = new Set();
1933
+ const appendCandidate = (rawId) => {
1934
+ if (typeof rawId !== 'string') return;
1935
+ const normalized = rawId.trim();
1936
+ if (!normalized || seen.has(normalized)) return;
1937
+ seen.add(normalized);
1938
+ result.push(normalized);
1939
+ };
1940
+
1941
+ const sourceData = this._getSourceGraphData();
1942
+ if (sourceData && Array.isArray(sourceData.nodes)) {
1943
+ sourceData.nodes.forEach((node) => appendCandidate(node?.id));
1944
+ }
1945
+ if (result.length < maxCount && Array.isArray(this.nodes)) {
1946
+ this.nodes.forEach((node) => appendCandidate(node?.id));
1947
+ }
1948
+
1949
+ return result.slice(0, maxCount);
1950
+ },
1951
+
1952
+ _isTauriMode: function() {
1953
+ const hasTauriGlobal = typeof window !== 'undefined' && !!window.__TAURI__;
1954
+ const runtimeCaps = (typeof window !== 'undefined' && window.__NC_RUNTIME_CAPS) ? window.__NC_RUNTIME_CAPS : null;
1955
+ const hasDesktopRuntimeCaps = !!(runtimeCaps && runtimeCaps.supports_sidecar === true);
1956
+ const userAgent = typeof navigator !== 'undefined' ? String(navigator.userAgent || '') : '';
1957
+ return hasTauriGlobal || hasDesktopRuntimeCaps || userAgent.includes('Tauri');
1958
+ },
1959
+
1960
+ _getModeValue: function() {
1961
+ if (this._isTauriMode()) {
1962
+ return this.runtimeConfig.mode || (this.currentTargetId ? 'diffusion' : 'domain');
1963
+ }
1964
+ return document.getElementById('learning-mode')?.value || 'domain';
1965
+ },
1966
+
1967
+ _getStrategyValue: function() {
1968
+ if (this._isTauriMode()) {
1969
+ return this.runtimeConfig.strategy || 'foundational';
1970
+ }
1971
+ return document.getElementById('strategy')?.value || 'foundational';
1972
+ },
1973
+
1974
+ _getLayoutValue: function() {
1975
+ if (this._isTauriMode()) {
1976
+ // Layout is backend-defaulted to Track(Focus) in Tauri flow.
1977
+ return 'orbital';
1978
+ }
1979
+ return document.getElementById('layout-style')?.value || 'orbital';
1980
+ },
1981
+
1982
+ _getRetainHistoryEnabled: function() {
1983
+ if (this._isTauriMode()) {
1984
+ return this.runtimeConfig.retainHistory !== false;
1985
+ }
1986
+ return document.getElementById('set-retain-history')?.checked ?? true;
1987
+ },
1988
+
1989
+ _toggleHistorySidebar: function() {
1990
+ const sidebar = document.getElementById('learning-history-sidebar');
1991
+ if (!sidebar) return;
1992
+ sidebar.style.zIndex = '3000';
1993
+ if (sidebar.style.display === 'none' || sidebar.style.display === '') {
1994
+ sidebar.style.display = 'flex';
1995
+ sidebar.offsetHeight;
1996
+ setTimeout(() => {
1997
+ sidebar.style.transform = 'translateX(0)';
1998
+ }, 10);
1999
+ } else {
2000
+ sidebar.style.transform = 'translateX(100%)';
2001
+ setTimeout(() => {
2002
+ sidebar.style.display = 'none';
2003
+ }, 300);
2004
+ }
2005
+ },
2006
+
2007
+ _normalizeLearningWorkbenchUserId: function(rawValue) {
2008
+ const candidate = String(rawValue || '')
2009
+ .trim()
2010
+ .toLowerCase()
2011
+ .replace(/[^\p{L}\p{N}_-]+/gu, '_')
2012
+ .replace(/_+/g, '_');
2013
+ if (!candidate) {
2014
+ return 'path_user_default';
2015
+ }
2016
+ return candidate.slice(0, 64);
2017
+ },
2018
+
2019
+ _getLearningQualityBaselineStorageKey: function(userId) {
2020
+ const normalizedUserId = this._normalizeLearningWorkbenchUserId(userId);
2021
+ return `nc_learning_quality_baseline::${normalizedUserId}`;
2022
+ },
2023
+
2024
+ _normalizeLearningQualitySnapshotShape: function(snapshot) {
2025
+ if (!snapshot || typeof snapshot !== 'object') {
2026
+ return null;
2027
+ }
2028
+ const requiredKeys = [
2029
+ 'retestPassRatePct',
2030
+ 'misconceptionRecurrenceRatePct',
2031
+ 'evidenceBackedSuggestionRatioPct',
2032
+ 'averagePathMasteryGainPct',
2033
+ 'randomPathMasteryGainPct',
2034
+ ];
2035
+ const hasRequiredKeys = requiredKeys.every((key) => Number.isFinite(Number(snapshot[key])));
2036
+ if (!hasRequiredKeys) {
2037
+ return null;
2038
+ }
2039
+ const clamp = (value, minValue, maxValue, digits = 4) => {
2040
+ const numericValue = Number(value || 0);
2041
+ const bounded = Math.min(maxValue, Math.max(minValue, numericValue));
2042
+ return Number(bounded.toFixed(digits));
2043
+ };
2044
+ return {
2045
+ retestPassRatePct: clamp(snapshot.retestPassRatePct, 0, 100),
2046
+ misconceptionRecurrenceRatePct: clamp(snapshot.misconceptionRecurrenceRatePct, 0, 100),
2047
+ evidenceBackedSuggestionRatioPct: clamp(snapshot.evidenceBackedSuggestionRatioPct, 0, 100),
2048
+ averagePathMasteryGainPct: clamp(snapshot.averagePathMasteryGainPct, 0, 100),
2049
+ randomPathMasteryGainPct: clamp(snapshot.randomPathMasteryGainPct, 0, 100),
2050
+ historyWindowDays: Math.max(1, Math.min(180, Math.floor(Number(snapshot.historyWindowDays || 14)))),
2051
+ historyWindowRecords: Math.max(0, Math.floor(Number(snapshot.historyWindowRecords || 0))),
2052
+ historyWindowAverageMasteryDelta: clamp(snapshot.historyWindowAverageMasteryDelta, -1, 1, 6),
2053
+ historyWindowRetestPositiveDeltaRatePct: clamp(snapshot.historyWindowRetestPositiveDeltaRatePct, 0, 100),
2054
+ queryP95Ms: clamp(snapshot.queryP95Ms, 0, 60000),
2055
+ };
2056
+ },
2057
+
2058
+ _loadLearningQualityBaseline: function(userId) {
2059
+ if (typeof localStorage === 'undefined') {
2060
+ return null;
2061
+ }
2062
+ try {
2063
+ const storageKey = this._getLearningQualityBaselineStorageKey(userId);
2064
+ const raw = localStorage.getItem(storageKey);
2065
+ if (!raw) {
2066
+ return null;
2067
+ }
2068
+ const parsed = JSON.parse(raw);
2069
+ if (!parsed || typeof parsed !== 'object') {
2070
+ return null;
2071
+ }
2072
+ return this._normalizeLearningQualitySnapshotShape(parsed.snapshot || null);
2073
+ } catch (error) {
2074
+ console.warn('[PathApp] Failed to load quality baseline:', error);
2075
+ return null;
2076
+ }
2077
+ },
2078
+
2079
+ _fetchLearningQualityBaselineFromServer: async function(userId) {
2080
+ const normalizedUserId = this._normalizeLearningWorkbenchUserId(userId);
2081
+ try {
2082
+ const result = await this._requestLearningApiGet('/api/knowledge/quality/baseline', {
2083
+ userId: normalizedUserId,
2084
+ });
2085
+ if (!result || result.found !== true) {
2086
+ return null;
2087
+ }
2088
+ return this._normalizeLearningQualitySnapshotShape(result.snapshot || null);
2089
+ } catch (error) {
2090
+ console.warn('[PathApp] Failed to fetch quality baseline from server:', error);
2091
+ return null;
2092
+ }
2093
+ },
2094
+
2095
+ _saveLearningQualityBaseline: function(userId, snapshot) {
2096
+ if (typeof localStorage === 'undefined') {
2097
+ return false;
2098
+ }
2099
+ const normalizedSnapshot = this._normalizeLearningQualitySnapshotShape(snapshot);
2100
+ if (!normalizedSnapshot) {
2101
+ return false;
2102
+ }
2103
+ try {
2104
+ const storageKey = this._getLearningQualityBaselineStorageKey(userId);
2105
+ localStorage.setItem(storageKey, JSON.stringify({
2106
+ userId: this._normalizeLearningWorkbenchUserId(userId),
2107
+ storedAt: new Date().toISOString(),
2108
+ snapshot: normalizedSnapshot,
2109
+ }));
2110
+ return true;
2111
+ } catch (error) {
2112
+ console.warn('[PathApp] Failed to save quality baseline:', error);
2113
+ return false;
2114
+ }
2115
+ },
2116
+
2117
+ _saveLearningQualityBaselineToServer: async function(userId, snapshot) {
2118
+ const normalizedUserId = this._normalizeLearningWorkbenchUserId(userId);
2119
+ const normalizedSnapshot = this._normalizeLearningQualitySnapshotShape(snapshot);
2120
+ if (!normalizedSnapshot) {
2121
+ return false;
2122
+ }
2123
+ try {
2124
+ const result = await this._requestLearningApi('/api/knowledge/quality/baseline', {
2125
+ userId: normalizedUserId,
2126
+ snapshot: normalizedSnapshot,
2127
+ });
2128
+ return Boolean(result && result.found === true);
2129
+ } catch (error) {
2130
+ console.warn('[PathApp] Failed to persist quality baseline to server:', error);
2131
+ return false;
2132
+ }
2133
+ },
2134
+
2135
+ _syncLearningQualityBaselineFromServer: async function(userId) {
2136
+ const normalizedUserId = this._normalizeLearningWorkbenchUserId(userId);
2137
+ const serverSnapshot = await this._fetchLearningQualityBaselineFromServer(normalizedUserId);
2138
+ if (!serverSnapshot) {
2139
+ this.learningWorkbench.qualityBaselineSnapshot = this._loadLearningQualityBaseline(normalizedUserId);
2140
+ return this.learningWorkbench.qualityBaselineSnapshot;
2141
+ }
2142
+ this.learningWorkbench.qualityBaselineSnapshot = serverSnapshot;
2143
+ this._saveLearningQualityBaseline(normalizedUserId, serverSnapshot);
2144
+ return serverSnapshot;
2145
+ },
2146
+
2147
+ evaluateLearningWorkbenchQualityGates: async function() {
2148
+ if (this.learningWorkbench.loading) {
2149
+ return;
2150
+ }
2151
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2152
+ const currentSnapshot = this.learningWorkbench.qualitySnapshot?.snapshot || null;
2153
+ const baselineSnapshot = await this._syncLearningQualityBaselineFromServer(userId);
2154
+ if (!currentSnapshot) {
2155
+ this._setLearningWorkbenchStatus('No current quality snapshot available. Refresh first.', true);
2156
+ return;
2157
+ }
2158
+ if (!baselineSnapshot) {
2159
+ this._setLearningWorkbenchStatus('No quality baseline found. Set baseline first.', true);
2160
+ return;
2161
+ }
2162
+ this.learningWorkbench.loading = true;
2163
+ this.learningWorkbench.qualityBaselineSnapshot = baselineSnapshot;
2164
+ this._setLearningWorkbenchStatus('Evaluating learning quality gates...');
2165
+ this._renderLearningWorkbenchState();
2166
+ try {
2167
+ let evaluation = null;
2168
+ let usedLegacyFallback = false;
2169
+ try {
2170
+ const evaluationEnvelope = await this._requestLearningApi('/api/knowledge/quality/baseline/evaluate', {
2171
+ userId,
2172
+ current: currentSnapshot,
2173
+ sampledAt: this.learningWorkbench.qualitySnapshot?.sampledAt || undefined,
2174
+ historyWindowDays: this.learningWorkbench.qualitySnapshot?.snapshot?.historyWindowDays || undefined,
2175
+ });
2176
+ evaluation = evaluationEnvelope && typeof evaluationEnvelope === 'object'
2177
+ ? evaluationEnvelope.evaluation
2178
+ : null;
2179
+ } catch (_baselineEvaluateError) {
2180
+ evaluation = await this._requestLearningApi('/api/knowledge/quality/evaluate', {
2181
+ baseline: baselineSnapshot,
2182
+ current: currentSnapshot,
2183
+ });
2184
+ usedLegacyFallback = true;
2185
+ }
2186
+ this.learningWorkbench.qualityGateEvaluation = evaluation || null;
2187
+ this._setLearningWorkbenchStatus(
2188
+ `Quality gate evaluation finished: ${evaluation?.overallPassed === true ? 'PASS' : 'FAIL'}`
2189
+ + `${usedLegacyFallback ? ' (legacy fallback route).' : '.'}`
2190
+ );
2191
+ } catch (error) {
2192
+ const message = String(error?.message || error || 'Unknown quality gate evaluation error');
2193
+ this.learningWorkbench.lastError = message;
2194
+ this._setLearningWorkbenchStatus(`Quality gate evaluation failed: ${message}`, true);
2195
+ } finally {
2196
+ this.learningWorkbench.loading = false;
2197
+ this._renderLearningWorkbenchState();
2198
+ }
2199
+ },
2200
+
2201
+ setLearningWorkbenchQualityBaselineFromCurrent: async function() {
2202
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2203
+ const snapshot = this.learningWorkbench.qualitySnapshot?.snapshot || null;
2204
+ if (!snapshot) {
2205
+ this._setLearningWorkbenchStatus('No quality snapshot available. Refresh first.', true);
2206
+ return;
2207
+ }
2208
+ this.learningWorkbench.loading = true;
2209
+ this._renderLearningWorkbenchState();
2210
+ const persistedServer = await this._saveLearningQualityBaselineToServer(userId, snapshot);
2211
+ const persistedLocal = this._saveLearningQualityBaseline(userId, snapshot);
2212
+ if (!persistedServer && !persistedLocal) {
2213
+ this.learningWorkbench.loading = false;
2214
+ this._renderLearningWorkbenchState();
2215
+ this._setLearningWorkbenchStatus('Failed to persist quality baseline snapshot.', true);
2216
+ return;
2217
+ }
2218
+ this.learningWorkbench.qualityBaselineSnapshot = await this._syncLearningQualityBaselineFromServer(userId);
2219
+ this.learningWorkbench.qualityGateEvaluation = null;
2220
+ this.learningWorkbench.loading = false;
2221
+ const storageHint = persistedServer
2222
+ ? 'server + local cache'
2223
+ : 'local cache';
2224
+ this._setLearningWorkbenchStatus(`Quality baseline updated from current snapshot (${storageHint}).`);
2225
+ this._renderLearningWorkbenchState();
2226
+ },
2227
+
2228
+ _normalizeLearningHistoryExecutionKind: function(rawValue) {
2229
+ const value = String(rawValue || 'all').trim().toLowerCase();
2230
+ if (value === 'session' || value === 'retest' || value === 'custom') {
2231
+ return value;
2232
+ }
2233
+ return 'all';
2234
+ },
2235
+
2236
+ _normalizeLearningHistoryDateValue: function(rawValue) {
2237
+ const value = String(rawValue || '').trim();
2238
+ if (!value) {
2239
+ return '';
2240
+ }
2241
+ if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
2242
+ return value;
2243
+ }
2244
+ const parsed = new Date(value);
2245
+ if (Number.isNaN(parsed.getTime())) {
2246
+ return '';
2247
+ }
2248
+ return parsed.toISOString().slice(0, 10);
2249
+ },
2250
+
2251
+ _getLearningWorkbenchSessionHistoryQuery: function() {
2252
+ const existing = this.learningWorkbench.sessionHistoryQuery && typeof this.learningWorkbench.sessionHistoryQuery === 'object'
2253
+ ? this.learningWorkbench.sessionHistoryQuery
2254
+ : {};
2255
+ const limit = Math.max(1, Math.min(20, Math.floor(Number(existing.limit) || 8)));
2256
+ const offset = Math.max(0, Math.floor(Number(existing.offset) || 0));
2257
+ const executionKind = this._normalizeLearningHistoryExecutionKind(existing.executionKind);
2258
+ const fromDate = this._normalizeLearningHistoryDateValue(existing.fromDate);
2259
+ const toDate = this._normalizeLearningHistoryDateValue(existing.toDate);
2260
+ const normalized = {
2261
+ limit,
2262
+ offset,
2263
+ executionKind,
2264
+ fromDate,
2265
+ toDate,
2266
+ };
2267
+ this.learningWorkbench.sessionHistoryQuery = normalized;
2268
+ return normalized;
2269
+ },
2270
+
2271
+ _buildLearningWorkbenchSessionHistoryRequest: function(userId) {
2272
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2273
+ const fromExecutedAt = query.fromDate
2274
+ ? `${query.fromDate}T00:00:00.000Z`
2275
+ : undefined;
2276
+ const toExecutedAt = query.toDate
2277
+ ? `${query.toDate}T23:59:59.999Z`
2278
+ : undefined;
2279
+ return {
2280
+ userId,
2281
+ limit: query.limit,
2282
+ offset: query.offset,
2283
+ executionKinds: query.executionKind === 'all' ? undefined : [query.executionKind],
2284
+ fromExecutedAt,
2285
+ toExecutedAt,
2286
+ };
2287
+ },
2288
+
2289
+ _setLearningWorkbenchHistoryExecutionKind: function(rawValue, options = {}) {
2290
+ const nextKind = this._normalizeLearningHistoryExecutionKind(rawValue);
2291
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2292
+ const changed = query.executionKind !== nextKind;
2293
+ this.learningWorkbench.sessionHistoryQuery = {
2294
+ ...query,
2295
+ executionKind: nextKind,
2296
+ offset: 0,
2297
+ };
2298
+ this._persistLearningWorkbenchPreferences();
2299
+ if (changed && options.refresh !== false) {
2300
+ void this.refreshLearningWorkbench({ force: false });
2301
+ return;
2302
+ }
2303
+ this._renderLearningWorkbenchState();
2304
+ },
2305
+
2306
+ _applyLearningWorkbenchHistoryDateRange: function(rawFromDate, rawToDate, options = {}) {
2307
+ const fromDate = this._normalizeLearningHistoryDateValue(rawFromDate);
2308
+ const toDate = this._normalizeLearningHistoryDateValue(rawToDate);
2309
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2310
+ const changed = query.fromDate !== fromDate || query.toDate !== toDate;
2311
+ this.learningWorkbench.sessionHistoryQuery = {
2312
+ ...query,
2313
+ fromDate,
2314
+ toDate,
2315
+ offset: 0,
2316
+ };
2317
+ this._persistLearningWorkbenchPreferences();
2318
+ if (changed && options.refresh !== false) {
2319
+ void this.refreshLearningWorkbench({ force: false });
2320
+ return;
2321
+ }
2322
+ this._renderLearningWorkbenchState();
2323
+ },
2324
+
2325
+ _changeLearningWorkbenchHistoryPage: function(direction) {
2326
+ const step = Math.sign(Number(direction) || 0);
2327
+ if (step === 0) {
2328
+ return;
2329
+ }
2330
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2331
+ const page = this.learningWorkbench.sessionHistory && this.learningWorkbench.sessionHistory.page
2332
+ ? this.learningWorkbench.sessionHistory.page
2333
+ : null;
2334
+ if (step > 0 && page && page.hasMore !== true) {
2335
+ return;
2336
+ }
2337
+ const nextOffset = step > 0
2338
+ ? query.offset + query.limit
2339
+ : Math.max(0, query.offset - query.limit);
2340
+ if (nextOffset === query.offset) {
2341
+ return;
2342
+ }
2343
+ this.learningWorkbench.sessionHistoryQuery = {
2344
+ ...query,
2345
+ offset: nextOffset,
2346
+ };
2347
+ this._persistLearningWorkbenchPreferences();
2348
+ void this.refreshLearningWorkbench({ force: false });
2349
+ },
2350
+
2351
+ _restoreLearningWorkbenchPreferences: function() {
2352
+ try {
2353
+ const stored = localStorage.getItem('nc_learning_workbench_prefs');
2354
+ if (!stored) {
2355
+ return;
2356
+ }
2357
+ const parsed = JSON.parse(stored);
2358
+ if (parsed && typeof parsed === 'object' && typeof parsed.userId === 'string') {
2359
+ this.learningWorkbench.userId = this._normalizeLearningWorkbenchUserId(parsed.userId);
2360
+ }
2361
+ if (parsed && typeof parsed === 'object') {
2362
+ const query = this._getLearningWorkbenchSessionHistoryQuery();
2363
+ this.learningWorkbench.sessionHistoryQuery = {
2364
+ ...query,
2365
+ executionKind: this._normalizeLearningHistoryExecutionKind(parsed.historyExecutionKind),
2366
+ fromDate: this._normalizeLearningHistoryDateValue(parsed.historyFromDate),
2367
+ toDate: this._normalizeLearningHistoryDateValue(parsed.historyToDate),
2368
+ offset: 0,
2369
+ };
2370
+ }
2371
+ this.learningWorkbench.qualityBaselineSnapshot = this._loadLearningQualityBaseline(this.learningWorkbench.userId);
2372
+ this.learningWorkbench.qualityGateEvaluation = null;
2373
+ } catch (error) {
2374
+ console.warn('[PathApp] Failed to restore learning workbench prefs:', error);
2375
+ }
2376
+ },
2377
+
2378
+ _persistLearningWorkbenchPreferences: function() {
2379
+ try {
2380
+ const historyQuery = this._getLearningWorkbenchSessionHistoryQuery();
2381
+ const payload = {
2382
+ userId: this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId),
2383
+ historyExecutionKind: historyQuery.executionKind,
2384
+ historyFromDate: historyQuery.fromDate,
2385
+ historyToDate: historyQuery.toDate,
2386
+ };
2387
+ localStorage.setItem('nc_learning_workbench_prefs', JSON.stringify(payload));
2388
+ } catch (error) {
2389
+ console.warn('[PathApp] Failed to persist learning workbench prefs:', error);
2390
+ }
2391
+ },
2392
+
2393
+ _toggleLearningWorkbenchSidebar: function() {
2394
+ const sidebar = document.getElementById('learning-workbench-sidebar');
2395
+ if (!sidebar) return;
2396
+ sidebar.style.zIndex = '3001';
2397
+ if (sidebar.style.display === 'none' || sidebar.style.display === '') {
2398
+ sidebar.style.display = 'flex';
2399
+ sidebar.offsetHeight;
2400
+ setTimeout(() => {
2401
+ sidebar.style.transform = 'translateX(0)';
2402
+ }, 10);
2403
+ void this.refreshLearningWorkbench({ force: false });
2404
+ return;
2405
+ }
2406
+ this._closeLearningWorkbenchSidebar();
2407
+ },
2408
+
2409
+ _closeLearningWorkbenchSidebar: function() {
2410
+ const sidebar = document.getElementById('learning-workbench-sidebar');
2411
+ if (!sidebar) return;
2412
+ sidebar.style.transform = 'translateX(-100%)';
2413
+ setTimeout(() => {
2414
+ sidebar.style.display = 'none';
2415
+ }, 300);
2416
+ },
2417
+
2418
+ _setLearningWorkbenchStatus: function(message, isError = false) {
2419
+ const statusEl = document.getElementById('learning-workbench-status');
2420
+ if (!statusEl) return;
2421
+ statusEl.textContent = String(message || '');
2422
+ statusEl.classList.toggle('is-error', !!isError);
2423
+ statusEl.classList.toggle('is-loading', this.learningWorkbench.loading === true);
2424
+ },
2425
+
2426
+ _requestLearningApi: async function(endpoint, payload = {}) {
2427
+ const response = await fetch(endpoint, {
2428
+ method: 'POST',
2429
+ headers: {
2430
+ 'Content-Type': 'application/json',
2431
+ },
2432
+ body: JSON.stringify(payload || {}),
2433
+ });
2434
+
2435
+ let body = null;
2436
+ try {
2437
+ body = await response.json();
2438
+ } catch (_error) {
2439
+ body = null;
2440
+ }
2441
+
2442
+ if (!response.ok) {
2443
+ const message = body && body.error
2444
+ ? body.error
2445
+ : `Request failed (${response.status})`;
2446
+ throw new Error(String(message));
2447
+ }
2448
+ if (!body || body.success !== true) {
2449
+ const message = body && body.error
2450
+ ? body.error
2451
+ : `API ${endpoint} returned an unexpected response`;
2452
+ throw new Error(String(message));
2453
+ }
2454
+ return body.result;
2455
+ },
2456
+
2457
+ _requestLearningApiGet: async function(endpoint, params = {}) {
2458
+ const query = new URLSearchParams();
2459
+ Object.keys(params || {}).forEach((key) => {
2460
+ const value = params[key];
2461
+ if (value === undefined || value === null || value === '') {
2462
+ return;
2463
+ }
2464
+ query.set(key, String(value));
2465
+ });
2466
+ const url = query.toString()
2467
+ ? `${endpoint}?${query.toString()}`
2468
+ : endpoint;
2469
+ const response = await fetch(url, {
2470
+ method: 'GET',
2471
+ });
2472
+
2473
+ let body = null;
2474
+ try {
2475
+ body = await response.json();
2476
+ } catch (_error) {
2477
+ body = null;
2478
+ }
2479
+
2480
+ if (!response.ok) {
2481
+ const message = body && body.error
2482
+ ? body.error
2483
+ : `Request failed (${response.status})`;
2484
+ throw new Error(String(message));
2485
+ }
2486
+ if (!body || body.success !== true) {
2487
+ const message = body && body.error
2488
+ ? body.error
2489
+ : `API ${url} returned an unexpected response`;
2490
+ throw new Error(String(message));
2491
+ }
2492
+ return body.result;
2493
+ },
2494
+
2495
+ _getFocusAtomIdsForLearning: function() {
2496
+ const ids = [];
2497
+ if (typeof this.currentTargetId === 'string' && this.currentTargetId.trim()) {
2498
+ ids.push(this.currentTargetId.trim());
2499
+ }
2500
+ if (Array.isArray(this.currentTargetIds)) {
2501
+ this.currentTargetIds.forEach((id) => {
2502
+ if (typeof id === 'string' && id.trim()) {
2503
+ ids.push(id.trim());
2504
+ }
2505
+ });
2506
+ }
2507
+ if (typeof this.centralNodeId === 'string' && this.centralNodeId.trim()) {
2508
+ ids.push(this.centralNodeId.trim());
2509
+ }
2510
+ return Array.from(new Set(ids));
2511
+ },
2512
+
2513
+ _getCurrentFocusNode: function() {
2514
+ const sourceData = this._getSourceGraphData();
2515
+ if (!sourceData || !Array.isArray(sourceData.nodes)) {
2516
+ return null;
2517
+ }
2518
+ const preferredIds = this._getFocusAtomIdsForLearning();
2519
+ for (const candidateId of preferredIds) {
2520
+ const matched = sourceData.nodes.find((node) => node && node.id === candidateId);
2521
+ if (matched) {
2522
+ return matched;
2523
+ }
2524
+ }
2525
+ if (typeof this.centralNodeId === 'string' && this.centralNodeId.trim()) {
2526
+ const matched = sourceData.nodes.find((node) => node && node.id === this.centralNodeId);
2527
+ if (matched) {
2528
+ return matched;
2529
+ }
2530
+ }
2531
+ return this.nodes && this.nodes.length > 0 ? this.nodes[0] : null;
2532
+ },
2533
+
2534
+ _buildLearningDocumentFromNode: function(node) {
2535
+ if (!node || typeof node !== 'object') {
2536
+ return null;
2537
+ }
2538
+ const nodeId = typeof node.id === 'string' ? node.id.trim() : '';
2539
+ if (!nodeId) {
2540
+ return null;
2541
+ }
2542
+ const normalizedDocId = `path_${nodeId.toLowerCase().replace(/[^\p{L}\p{N}_-]+/gu, '_').slice(0, 96)}`;
2543
+ const sourcePathRaw = node?.metadata?.filepath
2544
+ || node?.filepath
2545
+ || `Knowledge_Base/path_mode/${normalizedDocId}.md`;
2546
+ const sourcePath = String(sourcePathRaw || '').replace(/\\/g, '/');
2547
+ const title = typeof node.label === 'string' && node.label.trim()
2548
+ ? node.label.trim()
2549
+ : nodeId;
2550
+ const contentRaw = typeof node.content === 'string'
2551
+ ? node.content
2552
+ : (typeof node.summary === 'string' ? node.summary : '');
2553
+ const content = String(contentRaw || '').trim();
2554
+ const finalContent = content.length > 0
2555
+ ? `# ${title}\n\n${content}`
2556
+ : `# ${title}\n\nNode ${nodeId} was captured from Path Mode for learning orchestration.`;
2557
+ return {
2558
+ documentId: normalizedDocId,
2559
+ sourcePath,
2560
+ language: 'en',
2561
+ content: finalContent,
2562
+ };
2563
+ },
2564
+
2565
+ ingestFocusNodeForLearningWorkbench: async function() {
2566
+ const focusNode = this._getCurrentFocusNode();
2567
+ if (!focusNode) {
2568
+ this._setLearningWorkbenchStatus('No focus node available to ingest.', true);
2569
+ return;
2570
+ }
2571
+
2572
+ const document = this._buildLearningDocumentFromNode(focusNode);
2573
+ if (!document) {
2574
+ this._setLearningWorkbenchStatus('Failed to build ingest document from focus node.', true);
2575
+ return;
2576
+ }
2577
+
2578
+ this.learningWorkbench.loading = true;
2579
+ this.learningWorkbench.lastError = '';
2580
+ this._setLearningWorkbenchStatus('Ingesting focus node into learning graph...');
2581
+ try {
2582
+ const result = await this._requestLearningApi('/api/knowledge/ingest', {
2583
+ incremental: true,
2584
+ relationRecomputeMode: 'incremental',
2585
+ documents: [document],
2586
+ });
2587
+ const changedDocs = Number(result?.summary?.changedDocuments || 0);
2588
+ this._setLearningWorkbenchStatus(`Ingest succeeded. Changed documents: ${changedDocs}.`);
2589
+ await this.refreshLearningWorkbench({ force: true });
2590
+ } catch (error) {
2591
+ const message = String(error?.message || error || 'Unknown ingest error');
2592
+ this.learningWorkbench.lastError = message;
2593
+ this._setLearningWorkbenchStatus(`Ingest failed: ${message}`, true);
2594
+ } finally {
2595
+ this.learningWorkbench.loading = false;
2596
+ this._renderLearningWorkbenchState();
2597
+ }
2598
+ },
2599
+
2600
+ refreshLearningWorkbench: async function(options = {}) {
2601
+ if (this.learningWorkbench.loading) {
2602
+ return;
2603
+ }
2604
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2605
+ this.learningWorkbench.userId = userId;
2606
+ this.learningWorkbench.qualityBaselineSnapshot = await this._syncLearningQualityBaselineFromServer(userId);
2607
+ this.learningWorkbench.qualityGateEvaluation = null;
2608
+ this._persistLearningWorkbenchPreferences();
2609
+ this.learningWorkbench.loading = true;
2610
+ this.learningWorkbench.lastError = '';
2611
+ this._setLearningWorkbenchStatus('Refreshing learning workbench...');
2612
+ this._renderLearningWorkbenchState();
2613
+
2614
+ const focusAtomIds = this._getFocusAtomIdsForLearning();
2615
+ const includeDivergence = true;
2616
+ const includeRetrain = true;
2617
+ const sessionHistoryRequest = this._buildLearningWorkbenchSessionHistoryRequest(userId);
2618
+ try {
2619
+ const [sessionPlan, qualitySnapshot, misconceptions, runtimeState, sessionHistory] = await Promise.all([
2620
+ this._requestLearningApi('/api/knowledge/session/plan', {
2621
+ userId,
2622
+ focusAtomIds,
2623
+ maxActions: 14,
2624
+ includeDivergence,
2625
+ includeRetrain,
2626
+ }),
2627
+ this._requestLearningApi('/api/knowledge/quality/snapshot', {
2628
+ userId,
2629
+ }),
2630
+ this._requestLearningApi('/api/knowledge/mastery/misconceptions', {
2631
+ userId,
2632
+ atomIds: focusAtomIds,
2633
+ topK: 6,
2634
+ }),
2635
+ fetch('/api/knowledge/state', { method: 'GET' })
2636
+ .then((response) => response.json())
2637
+ .catch(() => null),
2638
+ this._requestLearningApi('/api/knowledge/session/history', sessionHistoryRequest),
2639
+ ]);
2640
+ this.learningWorkbench.sessionPlan = sessionPlan || null;
2641
+ this.learningWorkbench.qualitySnapshot = qualitySnapshot || null;
2642
+ this.learningWorkbench.misconceptions = misconceptions || null;
2643
+ this.learningWorkbench.runtimeState = runtimeState && runtimeState.success === true
2644
+ ? runtimeState
2645
+ : null;
2646
+ this.learningWorkbench.sessionHistory = sessionHistory || null;
2647
+ this.learningWorkbench.lastUpdatedAt = new Date().toISOString();
2648
+ const currentSnapshot = qualitySnapshot && qualitySnapshot.snapshot
2649
+ ? this._normalizeLearningQualitySnapshotShape(qualitySnapshot.snapshot)
2650
+ : null;
2651
+ if (currentSnapshot && this.learningWorkbench.qualityBaselineSnapshot) {
2652
+ try {
2653
+ const qualityGateEvaluation = await this._requestLearningApi('/api/knowledge/quality/evaluate', {
2654
+ baseline: this.learningWorkbench.qualityBaselineSnapshot,
2655
+ current: currentSnapshot,
2656
+ });
2657
+ this.learningWorkbench.qualityGateEvaluation = qualityGateEvaluation || null;
2658
+ } catch (qualityGateError) {
2659
+ console.warn('[PathApp] Quality gate evaluation during refresh failed:', qualityGateError);
2660
+ this.learningWorkbench.qualityGateEvaluation = null;
2661
+ }
2662
+ }
2663
+
2664
+ const actionCount = Number(sessionPlan?.summary?.totalActions || sessionPlan?.actions?.length || 0);
2665
+ this._setLearningWorkbenchStatus(`Learning workbench updated. Planned actions: ${actionCount}.`);
2666
+ } catch (error) {
2667
+ const message = String(error?.message || error || 'Unknown refresh error');
2668
+ this.learningWorkbench.lastError = message;
2669
+ this._setLearningWorkbenchStatus(`Refresh failed: ${message}`, true);
2670
+ } finally {
2671
+ this.learningWorkbench.loading = false;
2672
+ this._renderLearningWorkbenchState();
2673
+ }
2674
+ },
2675
+
2676
+ _escapeHtml: function(value) {
2677
+ return String(value || '')
2678
+ .replace(/&/g, '&amp;')
2679
+ .replace(/</g, '&lt;')
2680
+ .replace(/>/g, '&gt;')
2681
+ .replace(/"/g, '&quot;')
2682
+ .replace(/'/g, '&#39;');
2683
+ },
2684
+
2685
+ _collectOptionalAnswerForLearningAction: function(actionKind, atomId) {
2686
+ const normalized = String(actionKind || '').trim().toLowerCase();
2687
+ if (!['quiz', 'review', 'explain'].includes(normalized)) {
2688
+ return '';
2689
+ }
2690
+ if (typeof window === 'undefined' || typeof window.prompt !== 'function') {
2691
+ return '';
2692
+ }
2693
+ const promptMessage = `Optional: enter learner answer for ${actionKind} on ${atomId}.\nLeave empty to skip mastery diagnostics.`;
2694
+ const answer = window.prompt(promptMessage, '');
2695
+ if (typeof answer !== 'string') {
2696
+ return '';
2697
+ }
2698
+ return answer.trim();
2699
+ },
2700
+
2701
+ _collectOptionalAnswersForSessionActions: function(actions = []) {
2702
+ if (typeof window === 'undefined' || typeof window.prompt !== 'function') {
2703
+ return {};
2704
+ }
2705
+ if (!Array.isArray(actions) || actions.length === 0) {
2706
+ return {};
2707
+ }
2708
+ const confirmation = window.prompt(
2709
+ 'Provide learner answers for this session run? Type "yes" to enable, leave empty to skip.',
2710
+ ''
2711
+ );
2712
+ if (!confirmation || !/^y(es)?$/i.test(String(confirmation).trim())) {
2713
+ return {};
2714
+ }
2715
+ const answerMap = {};
2716
+ const promptableActions = actions
2717
+ .filter((action) => ['quiz', 'review', 'explain'].includes(String(action?.kind || '').trim().toLowerCase()))
2718
+ .slice(0, 6);
2719
+ promptableActions.forEach((action) => {
2720
+ const actionId = String(action?.id || '').trim();
2721
+ if (!actionId) {
2722
+ return;
2723
+ }
2724
+ const atomId = String(action?.atomId || 'unknown_atom').trim();
2725
+ const actionKind = String(action?.kind || 'action').trim();
2726
+ const promptText = `Optional answer for ${actionKind} on ${atomId} (leave empty to skip):`;
2727
+ const answer = window.prompt(promptText, '');
2728
+ if (typeof answer !== 'string') {
2729
+ return;
2730
+ }
2731
+ const normalized = answer.trim();
2732
+ if (normalized.length > 0) {
2733
+ answerMap[actionId] = normalized;
2734
+ }
2735
+ });
2736
+ return answerMap;
2737
+ },
2738
+
2739
+ _appendLearningWorkbenchSessionRecord: function(record) {
2740
+ if (!record || typeof record !== 'object') {
2741
+ return;
1552
2742
  }
1553
- const uniqueIds = [];
1554
- const seen = new Set();
1555
- candidateIds.forEach((id) => {
1556
- if (typeof id !== 'string') return;
1557
- const normalized = id.trim();
1558
- if (!normalized || seen.has(normalized)) return;
1559
- seen.add(normalized);
1560
- uniqueIds.push(normalized);
2743
+ const recordId = String(record.id || '').trim();
2744
+ if (!recordId) {
2745
+ return;
2746
+ }
2747
+ const historyQuery = this._getLearningWorkbenchSessionHistoryQuery();
2748
+ if (historyQuery.offset > 0) {
2749
+ return;
2750
+ }
2751
+ const normalizedRecordKind = this._normalizeLearningHistoryExecutionKind(record.executionKind);
2752
+ const recordExecutionKind = normalizedRecordKind === 'all' ? 'session' : normalizedRecordKind;
2753
+ if (historyQuery.executionKind !== 'all' && historyQuery.executionKind !== recordExecutionKind) {
2754
+ return;
2755
+ }
2756
+ const base = this.learningWorkbench.sessionHistory && typeof this.learningWorkbench.sessionHistory === 'object'
2757
+ ? this.learningWorkbench.sessionHistory
2758
+ : {
2759
+ userId: this.learningWorkbench.userId,
2760
+ generatedAt: new Date().toISOString(),
2761
+ records: [],
2762
+ page: {
2763
+ limit: historyQuery.limit,
2764
+ offset: 0,
2765
+ returnedRecords: 0,
2766
+ totalFilteredRecords: 0,
2767
+ hasMore: false,
2768
+ nextOffset: null,
2769
+ },
2770
+ summary: {
2771
+ totalRecords: 0,
2772
+ totalExecutedActions: 0,
2773
+ totalUpdatedMasteryCount: 0,
2774
+ averageMasteryDelta: 0,
2775
+ averageTutorConfidence: 0,
2776
+ executionKindBreakdown: [],
2777
+ },
2778
+ };
2779
+ const existingRecords = Array.isArray(base.records) ? base.records.slice() : [];
2780
+ const deduped = [record, ...existingRecords.filter((item) => String(item?.id || '').trim() !== recordId)]
2781
+ .slice(0, historyQuery.limit);
2782
+ const executionKindBreakdown = ['session', 'retest', 'custom'].map((executionKind) => {
2783
+ const kindRecords = deduped.filter((item) => {
2784
+ const normalizedKind = this._normalizeLearningHistoryExecutionKind(item?.executionKind);
2785
+ const resolvedKind = normalizedKind === 'all' ? 'session' : normalizedKind;
2786
+ return resolvedKind === executionKind;
2787
+ });
2788
+ const totalExecutedActions = kindRecords.reduce((sum, item) => sum + Number(item?.executedCount || 0), 0);
2789
+ const averageMasteryDelta = kindRecords.length > 0
2790
+ ? kindRecords.reduce((sum, item) => sum + Number(item?.averageMasteryDelta || 0), 0) / kindRecords.length
2791
+ : 0;
2792
+ return {
2793
+ executionKind,
2794
+ recordCount: kindRecords.length,
2795
+ totalExecutedActions,
2796
+ averageMasteryDelta,
2797
+ };
1561
2798
  });
1562
- return uniqueIds;
2799
+ this.learningWorkbench.sessionHistory = {
2800
+ ...base,
2801
+ generatedAt: new Date().toISOString(),
2802
+ records: deduped,
2803
+ page: {
2804
+ limit: historyQuery.limit,
2805
+ offset: 0,
2806
+ returnedRecords: deduped.length,
2807
+ totalFilteredRecords: deduped.length,
2808
+ hasMore: false,
2809
+ nextOffset: null,
2810
+ },
2811
+ summary: {
2812
+ ...(base.summary || {}),
2813
+ totalRecords: deduped.length,
2814
+ totalExecutedActions: deduped.reduce((sum, item) => sum + Number(item?.executedCount || 0), 0),
2815
+ totalUpdatedMasteryCount: deduped.reduce((sum, item) => sum + Number(item?.updatedMasteryCount || 0), 0),
2816
+ averageMasteryDelta: deduped.length > 0
2817
+ ? deduped.reduce((sum, item) => sum + Number(item?.averageMasteryDelta || 0), 0) / deduped.length
2818
+ : 0,
2819
+ averageTutorConfidence: deduped.length > 0
2820
+ ? deduped.reduce((sum, item) => sum + Number(item?.averageTutorConfidence || 0), 0) / deduped.length
2821
+ : 0,
2822
+ executionKindBreakdown,
2823
+ },
2824
+ };
1563
2825
  },
1564
2826
 
1565
- _getDefaultTargetIds: function(limit = 1) {
1566
- const maxCount = Math.max(0, Number.isFinite(limit) ? Math.floor(limit) : 0);
1567
- if (maxCount <= 0) return [];
1568
- const result = [];
1569
- const seen = new Set();
1570
- const appendCandidate = (rawId) => {
1571
- if (typeof rawId !== 'string') return;
1572
- const normalized = rawId.trim();
1573
- if (!normalized || seen.has(normalized)) return;
1574
- seen.add(normalized);
1575
- result.push(normalized);
1576
- };
2827
+ executeLearningWorkbenchAction: async function(params = {}) {
2828
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2829
+ const atomId = String(params.atomId || '').trim();
2830
+ const actionKind = String(params.actionKind || '').trim();
2831
+ const source = String(params.source || '').trim();
2832
+ if (!atomId || !actionKind) {
2833
+ this._setLearningWorkbenchStatus('Invalid action payload for tutor execution.', true);
2834
+ return;
2835
+ }
2836
+ const answer = this._collectOptionalAnswerForLearningAction(actionKind, atomId);
2837
+ const shouldAnalyzeAnswer = answer.length > 0;
1577
2838
 
1578
- const sourceData = this._getSourceGraphData();
1579
- if (sourceData && Array.isArray(sourceData.nodes)) {
1580
- sourceData.nodes.forEach((node) => appendCandidate(node?.id));
2839
+ this.learningWorkbench.loading = true;
2840
+ this._setLearningWorkbenchStatus(`Running session action (${actionKind}) for ${atomId}...`);
2841
+ this._renderLearningWorkbenchState();
2842
+ try {
2843
+ const result = await this._requestLearningApi('/api/knowledge/session/action', {
2844
+ userId,
2845
+ action: {
2846
+ atomId,
2847
+ kind: actionKind,
2848
+ source: source || 'session_plan',
2849
+ prompt: `Learning workbench action: ${actionKind} from ${source || 'session_plan'}`,
2850
+ answer: shouldAnalyzeAnswer ? answer : undefined,
2851
+ },
2852
+ autoAnalyzeAnswer: shouldAnalyzeAnswer,
2853
+ autoUpdateMasteryFromAnswer: shouldAnalyzeAnswer,
2854
+ persistMemory: true,
2855
+ memoryLayer: 'session',
2856
+ });
2857
+ const tutorResult = result && result.tutor ? result.tutor : null;
2858
+ this.learningWorkbench.tutorFeedback = {
2859
+ atomId,
2860
+ actionKind,
2861
+ source,
2862
+ result,
2863
+ tutorResult,
2864
+ receivedAt: new Date().toISOString(),
2865
+ };
2866
+ const tutorActionKind = result?.trace?.tutorActionKind || 'unknown';
2867
+ const persistedMemory = result?.trace?.persistedMemory === true;
2868
+ const updatedMastery = result?.trace?.updatedMastery === true;
2869
+ const effectiveOutcome = result?.trace?.effectiveOutcome || 'n/a';
2870
+ const masterySource = result?.trace?.masterySource || 'none';
2871
+ this._setLearningWorkbenchStatus(
2872
+ `Session action finished (${tutorActionKind}); memory ${persistedMemory ? 'persisted' : 'skipped'}; mastery ${updatedMastery ? `updated (${effectiveOutcome}, ${masterySource})` : 'unchanged'}.`
2873
+ );
2874
+ } catch (error) {
2875
+ const message = String(error?.message || error || 'Unknown tutor execution error');
2876
+ this.learningWorkbench.lastError = message;
2877
+ this._setLearningWorkbenchStatus(`Tutor action failed: ${message}`, true);
2878
+ } finally {
2879
+ this.learningWorkbench.loading = false;
2880
+ this._renderLearningWorkbenchState();
1581
2881
  }
1582
- if (result.length < maxCount && Array.isArray(this.nodes)) {
1583
- this.nodes.forEach((node) => appendCandidate(node?.id));
2882
+ },
2883
+
2884
+ runLearningWorkbenchSession: async function(options = {}) {
2885
+ if (this.learningWorkbench.loading) {
2886
+ return;
2887
+ }
2888
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2889
+ const sessionPlan = this.learningWorkbench.sessionPlan;
2890
+ if (!sessionPlan || !Array.isArray(sessionPlan.actions) || sessionPlan.actions.length === 0) {
2891
+ this._setLearningWorkbenchStatus('No session plan actions to execute. Refresh first.', true);
2892
+ return;
2893
+ }
2894
+ let actionLimit = Number(options.actionLimit);
2895
+ if (!Number.isFinite(actionLimit) || actionLimit <= 0) {
2896
+ if (typeof window !== 'undefined' && typeof window.prompt === 'function') {
2897
+ const suggestedLimit = Math.min(3, sessionPlan.actions.length);
2898
+ const raw = window.prompt(
2899
+ `Run how many planned actions? (1-${Math.min(20, sessionPlan.actions.length)})`,
2900
+ String(suggestedLimit)
2901
+ );
2902
+ if (raw === null) {
2903
+ this._setLearningWorkbenchStatus('Session execution canceled.');
2904
+ return;
2905
+ }
2906
+ actionLimit = Number(raw);
2907
+ } else {
2908
+ actionLimit = Math.min(3, sessionPlan.actions.length);
2909
+ }
1584
2910
  }
2911
+ actionLimit = Math.max(1, Math.min(20, Math.floor(Number(actionLimit) || 1), sessionPlan.actions.length));
2912
+ const selectedActions = sessionPlan.actions.slice(0, actionLimit);
2913
+ const answersByActionId = this._collectOptionalAnswersForSessionActions(selectedActions);
2914
+ const answerCount = Object.keys(answersByActionId).length;
2915
+ const autoAnalyzeAnswer = answerCount > 0;
1585
2916
 
1586
- return result.slice(0, maxCount);
2917
+ this.learningWorkbench.loading = true;
2918
+ this._setLearningWorkbenchStatus(`Running session plan execution for top ${actionLimit} actions...`);
2919
+ this._renderLearningWorkbenchState();
2920
+ try {
2921
+ const result = await this._requestLearningApi('/api/knowledge/session/execute', {
2922
+ userId,
2923
+ executionKind: 'session',
2924
+ sessionPlan,
2925
+ actionLimit,
2926
+ answersByActionId,
2927
+ autoAnalyzeAnswer,
2928
+ autoUpdateMasteryFromAnswer: autoAnalyzeAnswer,
2929
+ persistMemory: true,
2930
+ memoryLayer: 'session',
2931
+ });
2932
+ this.learningWorkbench.sessionExecution = {
2933
+ ...result,
2934
+ receivedAt: new Date().toISOString(),
2935
+ };
2936
+ this._appendLearningWorkbenchSessionRecord(result?.record || null);
2937
+ const firstExecuted = Array.isArray(result?.items)
2938
+ ? result.items.find((item) => item && item.status === 'executed' && item.result)
2939
+ : null;
2940
+ if (firstExecuted && firstExecuted.result) {
2941
+ this.learningWorkbench.tutorFeedback = {
2942
+ atomId: firstExecuted.action?.atomId || '',
2943
+ actionKind: firstExecuted.action?.kind || '',
2944
+ source: firstExecuted.action?.source || 'session_plan',
2945
+ result: firstExecuted.result,
2946
+ tutorResult: firstExecuted.result?.tutor || null,
2947
+ receivedAt: new Date().toISOString(),
2948
+ };
2949
+ }
2950
+ const summary = result?.summary || {};
2951
+ const retestCount = Number(result?.retestPlan?.summary?.totalActions || 0);
2952
+ this._setLearningWorkbenchStatus(
2953
+ `Session execution finished: executed ${Number(summary.executedCount || 0)}/${Number(summary.attemptedActions || 0)}, mastery updates ${Number(summary.updatedMasteryCount || 0)}, analyzed answers ${Number(summary.analyzedAnswerCount || 0)} (input=${answerCount}), retest actions ${retestCount}.`
2954
+ );
2955
+ } catch (error) {
2956
+ const message = String(error?.message || error || 'Unknown session execution error');
2957
+ this.learningWorkbench.lastError = message;
2958
+ this._setLearningWorkbenchStatus(`Session execution failed: ${message}`, true);
2959
+ } finally {
2960
+ this.learningWorkbench.loading = false;
2961
+ this._renderLearningWorkbenchState();
2962
+ }
1587
2963
  },
1588
2964
 
1589
- _isTauriMode: function() {
1590
- const hasTauriGlobal = typeof window !== 'undefined' && !!window.__TAURI__;
1591
- const runtimeCaps = (typeof window !== 'undefined' && window.__NC_RUNTIME_CAPS) ? window.__NC_RUNTIME_CAPS : null;
1592
- const hasDesktopRuntimeCaps = !!(runtimeCaps && runtimeCaps.supports_sidecar === true);
1593
- const userAgent = typeof navigator !== 'undefined' ? String(navigator.userAgent || '') : '';
1594
- return hasTauriGlobal || hasDesktopRuntimeCaps || userAgent.includes('Tauri');
1595
- },
2965
+ runLearningWorkbenchRetestSession: async function() {
2966
+ if (this.learningWorkbench.loading) {
2967
+ return;
2968
+ }
2969
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
2970
+ const existingExecution = this.learningWorkbench.sessionExecution || null;
2971
+ const retestActions = Array.isArray(existingExecution?.retestPlan?.actions)
2972
+ ? existingExecution.retestPlan.actions
2973
+ : [];
2974
+ if (retestActions.length === 0) {
2975
+ this._setLearningWorkbenchStatus('No immediate retest actions available. Run a session first.', true);
2976
+ return;
2977
+ }
2978
+ const answersByActionId = this._collectOptionalAnswersForSessionActions(retestActions);
2979
+ const answerCount = Object.keys(answersByActionId).length;
2980
+ const autoAnalyzeAnswer = answerCount > 0;
2981
+ const generatedAt = new Date().toISOString();
2982
+ const totalEstimatedMinutes = retestActions.reduce(
2983
+ (sum, action) => sum + Number(action?.estimatedMinutes || 0),
2984
+ 0
2985
+ );
2986
+ const retestSessionPlan = {
2987
+ userId,
2988
+ generatedAt,
2989
+ actions: retestActions,
2990
+ signals: {
2991
+ misconceptions: [],
2992
+ dueRetrainAtoms: Array.from(new Set(retestActions.map((action) => String(action.atomId || '').trim()))),
2993
+ masteryPathTargets: [],
2994
+ divergenceTargets: [],
2995
+ },
2996
+ summary: {
2997
+ totalActions: retestActions.length,
2998
+ totalEstimatedMinutes,
2999
+ evidenceCoverageRatio: 1,
3000
+ },
3001
+ };
1596
3002
 
1597
- _getModeValue: function() {
1598
- if (this._isTauriMode()) {
1599
- return this.runtimeConfig.mode || (this.currentTargetId ? 'diffusion' : 'domain');
3003
+ this.learningWorkbench.loading = true;
3004
+ this._setLearningWorkbenchStatus(`Running immediate retest for ${retestActions.length} actions...`);
3005
+ this._renderLearningWorkbenchState();
3006
+ try {
3007
+ const result = await this._requestLearningApi('/api/knowledge/session/execute', {
3008
+ userId,
3009
+ executionKind: 'retest',
3010
+ sessionPlan: retestSessionPlan,
3011
+ actionLimit: retestActions.length,
3012
+ answersByActionId,
3013
+ autoAnalyzeAnswer,
3014
+ autoUpdateMasteryFromAnswer: autoAnalyzeAnswer,
3015
+ persistMemory: true,
3016
+ memoryLayer: 'session',
3017
+ includeRetestPlan: false,
3018
+ });
3019
+ this.learningWorkbench.sessionExecution = {
3020
+ ...result,
3021
+ receivedAt: new Date().toISOString(),
3022
+ };
3023
+ this._appendLearningWorkbenchSessionRecord(result?.record || null);
3024
+ const summary = result?.summary || {};
3025
+ this._setLearningWorkbenchStatus(
3026
+ `Retest execution finished: executed ${Number(summary.executedCount || 0)}/${Number(summary.attemptedActions || 0)}, mastery delta ${Number(summary.averageMasteryDelta || 0).toFixed(3)}.`
3027
+ );
3028
+ } catch (error) {
3029
+ const message = String(error?.message || error || 'Unknown retest execution error');
3030
+ this.learningWorkbench.lastError = message;
3031
+ this._setLearningWorkbenchStatus(`Retest execution failed: ${message}`, true);
3032
+ } finally {
3033
+ this.learningWorkbench.loading = false;
3034
+ this._renderLearningWorkbenchState();
1600
3035
  }
1601
- return document.getElementById('learning-mode')?.value || 'domain';
1602
3036
  },
1603
3037
 
1604
- _getStrategyValue: function() {
1605
- if (this._isTauriMode()) {
1606
- return this.runtimeConfig.strategy || 'foundational';
3038
+ runLearningWorkbenchHistoryRetest: async function(recordId) {
3039
+ if (this.learningWorkbench.loading) {
3040
+ return;
3041
+ }
3042
+ const normalizedRecordId = String(recordId || '').trim();
3043
+ if (!normalizedRecordId) {
3044
+ this._setLearningWorkbenchStatus('Invalid history record id for replay.', true);
3045
+ return;
3046
+ }
3047
+ const userId = this._normalizeLearningWorkbenchUserId(this.learningWorkbench.userId);
3048
+ const records = Array.isArray(this.learningWorkbench.sessionHistory?.records)
3049
+ ? this.learningWorkbench.sessionHistory.records
3050
+ : [];
3051
+ const targetRecord = records.find((item) => String(item?.id || '').trim() === normalizedRecordId) || null;
3052
+ if (!targetRecord) {
3053
+ this._setLearningWorkbenchStatus('Target history record was not found in current page.', true);
3054
+ return;
3055
+ }
3056
+ const focusAtomIds = Array.from(
3057
+ new Set(
3058
+ (Array.isArray(targetRecord.focusAtomIds) ? targetRecord.focusAtomIds : [])
3059
+ .map((atomId) => String(atomId || '').trim())
3060
+ .filter((atomId) => atomId.length > 0)
3061
+ )
3062
+ );
3063
+ if (focusAtomIds.length === 0) {
3064
+ this._setLearningWorkbenchStatus('Selected history record has no focus atoms for replay.', true);
3065
+ return;
1607
3066
  }
1608
- return document.getElementById('strategy')?.value || 'foundational';
1609
- },
1610
3067
 
1611
- _getLayoutValue: function() {
1612
- if (this._isTauriMode()) {
1613
- // Layout is backend-defaulted to Track(Focus) in Tauri flow.
1614
- return 'orbital';
3068
+ this.learningWorkbench.loading = true;
3069
+ this._setLearningWorkbenchStatus(`Replaying retest from history record ${normalizedRecordId}...`);
3070
+ this._renderLearningWorkbenchState();
3071
+ try {
3072
+ const sessionPlan = await this._requestLearningApi('/api/knowledge/session/plan', {
3073
+ userId,
3074
+ focusAtomIds,
3075
+ maxActions: Math.min(14, Math.max(4, focusAtomIds.length * 2)),
3076
+ includeDivergence: false,
3077
+ includeRetrain: true,
3078
+ });
3079
+ if (!sessionPlan || !Array.isArray(sessionPlan.actions) || sessionPlan.actions.length === 0) {
3080
+ throw new Error('Replay retest failed because no runnable actions were generated.');
3081
+ }
3082
+ const actionLimit = Math.max(1, Math.min(8, focusAtomIds.length, sessionPlan.actions.length));
3083
+ const selectedActions = sessionPlan.actions.slice(0, actionLimit);
3084
+ const answersByActionId = this._collectOptionalAnswersForSessionActions(selectedActions);
3085
+ const answerCount = Object.keys(answersByActionId).length;
3086
+ const autoAnalyzeAnswer = answerCount > 0;
3087
+ const result = await this._requestLearningApi('/api/knowledge/session/execute', {
3088
+ userId,
3089
+ executionKind: 'retest',
3090
+ sessionPlan,
3091
+ actionLimit,
3092
+ answersByActionId,
3093
+ autoAnalyzeAnswer,
3094
+ autoUpdateMasteryFromAnswer: autoAnalyzeAnswer,
3095
+ persistMemory: true,
3096
+ memoryLayer: 'session',
3097
+ includeRetestPlan: false,
3098
+ });
3099
+ this.learningWorkbench.sessionExecution = {
3100
+ ...result,
3101
+ receivedAt: new Date().toISOString(),
3102
+ };
3103
+ this._appendLearningWorkbenchSessionRecord(result?.record || null);
3104
+ const summary = result?.summary || {};
3105
+ this._setLearningWorkbenchStatus(
3106
+ `History replay finished: executed ${Number(summary.executedCount || 0)}/${Number(summary.attemptedActions || 0)}, mastery delta ${Number(summary.averageMasteryDelta || 0).toFixed(3)}.`
3107
+ );
3108
+ } catch (error) {
3109
+ const message = String(error?.message || error || 'Unknown history replay error');
3110
+ this.learningWorkbench.lastError = message;
3111
+ this._setLearningWorkbenchStatus(`History replay failed: ${message}`, true);
3112
+ } finally {
3113
+ this.learningWorkbench.loading = false;
3114
+ this._renderLearningWorkbenchState();
1615
3115
  }
1616
- return document.getElementById('layout-style')?.value || 'orbital';
1617
3116
  },
1618
3117
 
1619
- _getRetainHistoryEnabled: function() {
1620
- if (this._isTauriMode()) {
1621
- return this.runtimeConfig.retainHistory !== false;
3118
+ _renderLearningWorkbenchState: function() {
3119
+ const userIdInput = document.getElementById('learning-user-id');
3120
+ if (userIdInput && userIdInput.value !== this.learningWorkbench.userId) {
3121
+ userIdInput.value = this.learningWorkbench.userId;
3122
+ }
3123
+
3124
+ const qualityEl = document.getElementById('learning-quality-summary');
3125
+ const qualityGatesEl = document.getElementById('learning-quality-gates');
3126
+ const setQualityBaselineBtn = document.getElementById('btn-set-quality-baseline');
3127
+ const evaluateQualityGatesBtn = document.getElementById('btn-evaluate-quality-gates');
3128
+ const misconceptionEl = document.getElementById('learning-misconception-list');
3129
+ const actionsEl = document.getElementById('learning-session-actions');
3130
+ const runtimeEl = document.getElementById('learning-runtime-summary');
3131
+ const updatedEl = document.getElementById('learning-workbench-updated-at');
3132
+ const tutorFeedbackEl = document.getElementById('learning-tutor-feedback');
3133
+ const sessionExecutionEl = document.getElementById('learning-session-execution');
3134
+ const sessionHistoryEl = document.getElementById('learning-session-history');
3135
+ const sessionHistorySummaryEl = document.getElementById('learning-session-history-summary');
3136
+ const sessionHistoryFilterEl = document.getElementById('learning-history-kind-filter');
3137
+ const sessionHistoryFromDateEl = document.getElementById('learning-history-from-date');
3138
+ const sessionHistoryToDateEl = document.getElementById('learning-history-to-date');
3139
+ const sessionHistoryApplyRangeBtn = document.getElementById('btn-learning-history-apply-range');
3140
+ const sessionHistoryClearRangeBtn = document.getElementById('btn-learning-history-clear-range');
3141
+ const sessionHistoryPrevBtn = document.getElementById('btn-learning-history-prev');
3142
+ const sessionHistoryNextBtn = document.getElementById('btn-learning-history-next');
3143
+ const sessionHistoryPageEl = document.getElementById('learning-history-page');
3144
+
3145
+ if (updatedEl) {
3146
+ updatedEl.textContent = this.learningWorkbench.lastUpdatedAt
3147
+ ? `Last updated: ${new Date(this.learningWorkbench.lastUpdatedAt).toLocaleString()}`
3148
+ : 'Last updated: -';
3149
+ }
3150
+
3151
+ if (qualityEl) {
3152
+ const snapshot = this.learningWorkbench.qualitySnapshot?.snapshot || null;
3153
+ const baseline = this.learningWorkbench.qualityBaselineSnapshot || null;
3154
+ if (setQualityBaselineBtn) {
3155
+ setQualityBaselineBtn.disabled = this.learningWorkbench.loading === true || !snapshot;
3156
+ }
3157
+ if (evaluateQualityGatesBtn) {
3158
+ evaluateQualityGatesBtn.disabled = this.learningWorkbench.loading === true || !snapshot || !baseline;
3159
+ }
3160
+ if (!snapshot) {
3161
+ qualityEl.innerHTML = '<li class="muted">No quality snapshot yet.</li>';
3162
+ } else {
3163
+ qualityEl.innerHTML = [
3164
+ `<li>Retest pass rate: <strong>${Number(snapshot.retestPassRatePct || 0).toFixed(2)}%</strong></li>`,
3165
+ `<li>Misconception recurrence: <strong>${Number(snapshot.misconceptionRecurrenceRatePct || 0).toFixed(2)}%</strong></li>`,
3166
+ `<li>Evidence-backed suggestions: <strong>${Number(snapshot.evidenceBackedSuggestionRatioPct || 0).toFixed(2)}%</strong></li>`,
3167
+ `<li>Path gain vs random: <strong>${Number(snapshot.averagePathMasteryGainPct || 0).toFixed(2)}% / ${Number(snapshot.randomPathMasteryGainPct || 0).toFixed(2)}%</strong></li>`,
3168
+ `<li>History window: <strong>${Math.max(1, Math.floor(Number(snapshot.historyWindowDays || 14)))}d, records ${Math.max(0, Math.floor(Number(snapshot.historyWindowRecords || 0)))}</strong></li>`,
3169
+ `<li>History mastery delta avg: <strong>${Number(snapshot.historyWindowAverageMasteryDelta || 0).toFixed(3)}</strong></li>`,
3170
+ `<li>History retest positive delta rate: <strong>${Number(snapshot.historyWindowRetestPositiveDeltaRatePct || 0).toFixed(2)}%</strong></li>`,
3171
+ `<li>Query p95: <strong>${Number(snapshot.queryP95Ms || 0).toFixed(2)} ms</strong></li>`,
3172
+ `<li>Baseline status: <strong>${baseline ? 'configured' : 'missing'}</strong></li>`,
3173
+ ].join('');
3174
+ }
1622
3175
  }
1623
- return document.getElementById('set-retain-history')?.checked ?? true;
1624
- },
1625
3176
 
1626
- _toggleHistorySidebar: function() {
1627
- const sidebar = document.getElementById('learning-history-sidebar');
1628
- if (!sidebar) return;
1629
- sidebar.style.zIndex = '3000';
1630
- if (sidebar.style.display === 'none' || sidebar.style.display === '') {
1631
- sidebar.style.display = 'flex';
1632
- sidebar.offsetHeight;
1633
- setTimeout(() => {
1634
- sidebar.style.transform = 'translateX(0)';
1635
- }, 10);
1636
- } else {
1637
- sidebar.style.transform = 'translateX(100%)';
1638
- setTimeout(() => {
1639
- sidebar.style.display = 'none';
1640
- }, 300);
3177
+ if (qualityGatesEl) {
3178
+ const evaluation = this.learningWorkbench.qualityGateEvaluation || null;
3179
+ const baseline = this.learningWorkbench.qualityBaselineSnapshot || null;
3180
+ if (!baseline) {
3181
+ qualityGatesEl.textContent = 'No quality baseline yet. Use "Set Baseline" after refreshing quality snapshot.';
3182
+ } else if (!evaluation) {
3183
+ qualityGatesEl.textContent = 'No quality gate evaluation yet.';
3184
+ } else {
3185
+ const gateLines = Array.isArray(evaluation.gates)
3186
+ ? evaluation.gates.map((gate) => {
3187
+ const status = gate.passed === true ? 'PASS' : 'FAIL';
3188
+ const comparator = String(gate.comparator || '>=');
3189
+ const observedValue = Number(gate.observedValue || 0).toFixed(gate.unit === 'ms' ? 2 : 4);
3190
+ const threshold = Number(gate.threshold || 0).toFixed(gate.unit === 'ms' ? 2 : 4);
3191
+ return `- [${status}] ${gate.gateId}: ${observedValue} ${comparator} ${threshold} ${gate.unit}`;
3192
+ })
3193
+ : [];
3194
+ const deltas = evaluation.deltas || {};
3195
+ qualityGatesEl.textContent = [
3196
+ `Evaluated at: ${evaluation.evaluatedAt || '-'}`,
3197
+ `Overall: ${evaluation.overallPassed === true ? 'PASS' : 'FAIL'}`,
3198
+ `Delta retest pass-rate uplift: ${Number(deltas.retestPassRateUpliftPct || 0).toFixed(4)}`,
3199
+ `Delta misconception reduction: ${Number(deltas.misconceptionRecurrenceReductionPct || 0).toFixed(4)}`,
3200
+ `Delta path effectiveness lift: ${Number(deltas.pathEffectivenessLiftPct || 0).toFixed(4)}`,
3201
+ `Delta history mastery uplift: ${Number(deltas.historyWindowAverageMasteryDeltaUplift || 0).toFixed(6)}`,
3202
+ 'Gates:',
3203
+ ...(gateLines.length > 0 ? gateLines : ['- n/a']),
3204
+ ].join('\n');
3205
+ }
3206
+ }
3207
+
3208
+ if (misconceptionEl) {
3209
+ const items = this.learningWorkbench.misconceptions?.items || [];
3210
+ if (!Array.isArray(items) || items.length === 0) {
3211
+ misconceptionEl.innerHTML = '<li class="muted">No recurring misconceptions detected.</li>';
3212
+ } else {
3213
+ misconceptionEl.innerHTML = items.map((item) => {
3214
+ const tag = String(item.errorTag || 'unknown');
3215
+ const count = Number(item.count || 0);
3216
+ const severity = Number(item.severityScore || 0).toFixed(3);
3217
+ return `<li><span class="chip">${tag}</span> count=${count}, severity=${severity}</li>`;
3218
+ }).join('');
3219
+ }
3220
+ }
3221
+
3222
+ if (actionsEl) {
3223
+ const actions = this.learningWorkbench.sessionPlan?.actions || [];
3224
+ if (!Array.isArray(actions) || actions.length === 0) {
3225
+ actionsEl.innerHTML = '<li class="muted">No session actions yet.</li>';
3226
+ } else {
3227
+ actionsEl.innerHTML = actions.slice(0, 10).map((action) => {
3228
+ const source = String(action.source || 'unknown');
3229
+ const kind = String(action.kind || 'action');
3230
+ const atomId = String(action.atomId || '');
3231
+ const minutes = Number(action.estimatedMinutes || 0);
3232
+ const safeKind = this._escapeHtml(kind);
3233
+ const safeSource = this._escapeHtml(source);
3234
+ const safeAtomId = this._escapeHtml(atomId);
3235
+ const encodedKind = encodeURIComponent(kind);
3236
+ const encodedAtomId = encodeURIComponent(atomId);
3237
+ const encodedSource = encodeURIComponent(source);
3238
+ return `
3239
+ <li>
3240
+ <div><strong>${safeKind}</strong> <span class="chip">${safeSource}</span> <code>${safeAtomId}</code> (${minutes}m)</div>
3241
+ <button
3242
+ class="btn-small workbench-run-action"
3243
+ data-action-kind="${encodedKind}"
3244
+ data-atom-id="${encodedAtomId}"
3245
+ data-action-source="${encodedSource}"
3246
+ type="button"
3247
+ >
3248
+ Ask Tutor
3249
+ </button>
3250
+ </li>
3251
+ `;
3252
+ }).join('');
3253
+ }
3254
+ }
3255
+
3256
+ if (tutorFeedbackEl) {
3257
+ const tutorFeedback = this.learningWorkbench.tutorFeedback;
3258
+ if (!tutorFeedback || !tutorFeedback.result) {
3259
+ tutorFeedbackEl.textContent = 'No tutor feedback yet.';
3260
+ } else {
3261
+ const trace = tutorFeedback.result?.trace || {};
3262
+ const tutorResult = tutorFeedback.tutorResult || tutorFeedback.result?.tutor || {};
3263
+ const message = String(tutorResult.message || '').trim();
3264
+ const safeMessage = message.length > 0 ? message : 'Tutor returned an empty message.';
3265
+ const answerAnalysis = tutorFeedback.result?.answerAnalysis || null;
3266
+ const answerAnalysisMessage = String(answerAnalysis?.message || '').trim();
3267
+ const safeAnswerAnalysis = answerAnalysisMessage.length > 0
3268
+ ? answerAnalysisMessage
3269
+ : 'No answer analysis was generated.';
3270
+ tutorFeedbackEl.textContent = [
3271
+ `Action: ${tutorFeedback.actionKind} (${trace.tutorActionKind || 'unknown'})`,
3272
+ `Atom: ${tutorFeedback.atomId}`,
3273
+ `Memory persisted: ${trace.persistedMemory === true ? 'yes' : 'no'}`,
3274
+ `Mastery updated: ${trace.updatedMastery === true ? 'yes' : 'no'} (${trace.masterySource || 'none'})`,
3275
+ `Outcome/Error: ${trace.effectiveOutcome || 'n/a'} / ${trace.effectiveErrorTag || 'n/a'}`,
3276
+ safeMessage,
3277
+ '',
3278
+ 'Answer Analysis:',
3279
+ safeAnswerAnalysis,
3280
+ ].join('\n');
3281
+ }
3282
+ }
3283
+
3284
+ if (sessionExecutionEl) {
3285
+ const execution = this.learningWorkbench.sessionExecution;
3286
+ if (!execution || !execution.summary) {
3287
+ sessionExecutionEl.textContent = 'No session execution yet.';
3288
+ } else {
3289
+ const summary = execution.summary || {};
3290
+ const masteryDelta = execution.masteryDelta || null;
3291
+ const retestPlan = execution.retestPlan || null;
3292
+ const topDeltaLines = Array.isArray(masteryDelta?.items)
3293
+ ? masteryDelta.items.slice(0, 3).map((item) => {
3294
+ const before = Number(item.beforeMastery || 0).toFixed(3);
3295
+ const after = Number(item.afterMastery || 0).toFixed(3);
3296
+ const delta = Number(item.deltaMastery || 0);
3297
+ const signedDelta = `${delta >= 0 ? '+' : ''}${delta.toFixed(3)}`;
3298
+ const atomId = String(item.atomId || 'unknown_atom');
3299
+ return `- ${atomId}: ${before} -> ${after} (${signedDelta})`;
3300
+ })
3301
+ : [];
3302
+ const retestPreviewLines = Array.isArray(retestPlan?.actions)
3303
+ ? retestPlan.actions.slice(0, 3).map((action) => {
3304
+ const atomId = String(action.atomId || 'unknown_atom');
3305
+ const kind = String(action.kind || 'action');
3306
+ const source = String(action.source || 'retrain_plan');
3307
+ return `- ${kind} @ ${atomId} (${source})`;
3308
+ })
3309
+ : [];
3310
+ sessionExecutionEl.textContent = [
3311
+ `Executed at: ${execution.executedAt || '-'}`,
3312
+ `Planned/Attempted/Executed: ${Number(summary.plannedActions || 0)}/${Number(summary.attemptedActions || 0)}/${Number(summary.executedCount || 0)}`,
3313
+ `Skipped/Failed: ${Number(summary.skippedCount || 0)}/${Number(summary.failedCount || 0)}`,
3314
+ `Mastery updates (inferred/explicit): ${Number(summary.updatedMasteryCount || 0)} (${Number(summary.inferredMasteryCount || 0)}/${Number(summary.explicitMasteryCount || 0)})`,
3315
+ `Answer analyzed: ${Number(summary.analyzedAnswerCount || 0)}, memory persisted: ${Number(summary.memoryPersistedCount || 0)}`,
3316
+ `Avg tutor confidence: ${Number(summary.averageTutorConfidence || 0).toFixed(3)}`,
3317
+ `Mastery avg before/after/delta: ${Number(summary.averageMasteryBefore || 0).toFixed(3)} / ${Number(summary.averageMasteryAfter || 0).toFixed(3)} / ${Number(summary.averageMasteryDelta || 0).toFixed(3)}`,
3318
+ `Mastery movement improved/regressed/flat: ${Number(summary.improvedAtomCount || 0)}/${Number(summary.regressedAtomCount || 0)}/${Number(summary.unchangedAtomCount || 0)}`,
3319
+ `Stopped early: ${summary.stoppedEarly === true ? 'yes' : 'no'}`,
3320
+ ...(masteryDelta
3321
+ ? [
3322
+ `Compared atoms: ${Number(masteryDelta.comparedAtoms || 0)}, avg delta: ${Number(masteryDelta.averageDelta || 0).toFixed(3)}`,
3323
+ 'Top mastery delta atoms:',
3324
+ ...(topDeltaLines.length > 0 ? topDeltaLines : ['- n/a']),
3325
+ ]
3326
+ : []),
3327
+ ...(retestPlan
3328
+ ? [
3329
+ `Immediate retest actions: ${Number(retestPlan.summary?.totalActions || 0)}`,
3330
+ 'Retest preview:',
3331
+ ...(retestPreviewLines.length > 0 ? retestPreviewLines : ['- n/a']),
3332
+ ]
3333
+ : []),
3334
+ ].join('\n');
3335
+ }
3336
+ }
3337
+
3338
+ if (sessionHistoryEl) {
3339
+ const records = this.learningWorkbench.sessionHistory?.records || [];
3340
+ const summary = this.learningWorkbench.sessionHistory?.summary || null;
3341
+ const page = this.learningWorkbench.sessionHistory?.page || null;
3342
+ const historyQuery = this._getLearningWorkbenchSessionHistoryQuery();
3343
+ if (sessionHistoryFilterEl) {
3344
+ sessionHistoryFilterEl.value = historyQuery.executionKind;
3345
+ }
3346
+ if (sessionHistoryFromDateEl) {
3347
+ sessionHistoryFromDateEl.value = historyQuery.fromDate;
3348
+ }
3349
+ if (sessionHistoryToDateEl) {
3350
+ sessionHistoryToDateEl.value = historyQuery.toDate;
3351
+ }
3352
+ if (sessionHistoryApplyRangeBtn) {
3353
+ sessionHistoryApplyRangeBtn.disabled = this.learningWorkbench.loading === true;
3354
+ }
3355
+ if (sessionHistoryClearRangeBtn) {
3356
+ const hasDateRange = historyQuery.fromDate.length > 0 || historyQuery.toDate.length > 0;
3357
+ sessionHistoryClearRangeBtn.disabled = this.learningWorkbench.loading === true || !hasDateRange;
3358
+ }
3359
+ if (sessionHistoryPrevBtn) {
3360
+ sessionHistoryPrevBtn.disabled = this.learningWorkbench.loading === true || historyQuery.offset <= 0;
3361
+ }
3362
+ if (sessionHistoryNextBtn) {
3363
+ const canGoNext = page ? page.hasMore === true : false;
3364
+ sessionHistoryNextBtn.disabled = this.learningWorkbench.loading === true || !canGoNext;
3365
+ }
3366
+ if (sessionHistoryPageEl) {
3367
+ const totalFilteredRecords = Math.max(0, Number(page?.totalFilteredRecords || 0));
3368
+ const totalPages = Math.max(1, Math.ceil(totalFilteredRecords / Math.max(1, historyQuery.limit)));
3369
+ const pageIndex = Math.floor(historyQuery.offset / Math.max(1, historyQuery.limit)) + 1;
3370
+ sessionHistoryPageEl.textContent = `Page ${Math.min(pageIndex, totalPages)}/${totalPages}`;
3371
+ }
3372
+ if (sessionHistorySummaryEl) {
3373
+ const totalRecords = Number(summary?.totalRecords || 0);
3374
+ const totalExecuted = Number(summary?.totalExecutedActions || 0);
3375
+ const totalUpdated = Number(summary?.totalUpdatedMasteryCount || 0);
3376
+ const avgDelta = Number(summary?.averageMasteryDelta || 0).toFixed(3);
3377
+ const avgConfidence = Number(summary?.averageTutorConfidence || 0).toFixed(3);
3378
+ const rangeSummary = historyQuery.fromDate || historyQuery.toDate
3379
+ ? `, range=${historyQuery.fromDate || '-'}..${historyQuery.toDate || '-'}`
3380
+ : '';
3381
+ sessionHistorySummaryEl.textContent = `History summary: records=${totalRecords}, executed=${totalExecuted}, updated=${totalUpdated}, avgDelta=${avgDelta}, avgConfidence=${avgConfidence}${rangeSummary}`;
3382
+ }
3383
+ if (!Array.isArray(records) || records.length === 0) {
3384
+ sessionHistoryEl.innerHTML = '<li class="muted">No session history yet.</li>';
3385
+ } else {
3386
+ sessionHistoryEl.innerHTML = records.slice(0, Math.max(1, historyQuery.limit)).map((record) => {
3387
+ const executedAt = new Date(record.executedAt || Date.now()).toLocaleString();
3388
+ const kind = this._escapeHtml(String(record.executionKind || 'session'));
3389
+ const executedCount = Number(record.executedCount || 0);
3390
+ const attempted = Number(record.attemptedActions || 0);
3391
+ const delta = Number(record.averageMasteryDelta || 0);
3392
+ const signedDelta = `${delta >= 0 ? '+' : ''}${delta.toFixed(3)}`;
3393
+ const recordId = String(record.id || '').trim();
3394
+ const encodedRecordId = encodeURIComponent(recordId);
3395
+ const focusAtomIds = Array.isArray(record.focusAtomIds) ? record.focusAtomIds : [];
3396
+ const focusAtomCount = focusAtomIds.length;
3397
+ const focusPreview = focusAtomIds
3398
+ .slice(0, 3)
3399
+ .map((atomId) => this._escapeHtml(String(atomId || '')))
3400
+ .filter((atomId) => atomId.length > 0)
3401
+ .join(', ');
3402
+ const replayDisabledAttr = focusAtomCount > 0 ? '' : ' disabled';
3403
+ return `
3404
+ <li>
3405
+ <div><span class="chip">${kind}</span> ${executedAt} - ${executedCount}/${attempted} - mastery delta ${signedDelta}</div>
3406
+ <div class="muted">Focus atoms: ${focusAtomCount}${focusPreview ? ` (${focusPreview})` : ''}</div>
3407
+ <button class="btn-small workbench-run-history-retest" data-history-record-id="${encodedRecordId}" type="button"${replayDisabledAttr}>Replay Retest</button>
3408
+ </li>
3409
+ `;
3410
+ }).join('');
3411
+ }
3412
+ }
3413
+
3414
+ if (runtimeEl) {
3415
+ const runtimeState = this.learningWorkbench.runtimeState?.state || null;
3416
+ if (!runtimeState) {
3417
+ runtimeEl.textContent = 'Runtime: unavailable';
3418
+ } else {
3419
+ const sessionTelemetry = runtimeState.sessionActionTelemetry || null;
3420
+ const historyCount = Number(runtimeState.sessionExecutionHistoryRecords || 0);
3421
+ const sessionSummary = sessionTelemetry
3422
+ ? `, sessionActions=${Number(sessionTelemetry.executionCount || 0)}, inferred=${Number(sessionTelemetry.inferredMasteryUpdateCount || 0)}, explicit=${Number(sessionTelemetry.explicitMasteryUpdateCount || 0)}, history=${historyCount}`
3423
+ : `, history=${historyCount}`;
3424
+ runtimeEl.textContent = `Runtime: docs=${runtimeState.documents}, atoms=${runtimeState.activeAtoms}, relations=${runtimeState.activeRelationEdges}, ingestP95=${Number(runtimeState.ingestTelemetry?.ingestP95Ms || 0).toFixed(2)}ms${sessionSummary}`;
3425
+ }
1641
3426
  }
1642
3427
  },
1643
3428
 
@@ -1708,6 +3493,7 @@ window.pathApp = {
1708
3493
  const pathContainer = document.getElementById('path-container');
1709
3494
  const graphWrapper = document.getElementById('graph-wrapper');
1710
3495
  const sidebar = document.getElementById('learning-history-sidebar');
3496
+ const workbenchSidebar = document.getElementById('learning-workbench-sidebar');
1711
3497
 
1712
3498
  if (pathContainer) pathContainer.style.display = 'none';
1713
3499
  if (graphWrapper) graphWrapper.style.display = 'block';
@@ -1715,6 +3501,10 @@ window.pathApp = {
1715
3501
  sidebar.style.transform = 'translateX(100%)';
1716
3502
  sidebar.style.display = 'none';
1717
3503
  }
3504
+ if (workbenchSidebar) {
3505
+ workbenchSidebar.style.transform = 'translateX(-100%)';
3506
+ workbenchSidebar.style.display = 'none';
3507
+ }
1718
3508
  const multiWindowOptions = this._resolveMultiWindowOptions();
1719
3509
 
1720
3510
  // Single-window toggle: hide Godot, show Tauri.
@@ -2237,6 +4027,8 @@ window.pathApp = {
2237
4027
  const tauriMode = this._isTauriMode();
2238
4028
  const toolbar = document.getElementById('path-toolbar');
2239
4029
 
4030
+ this._restoreLearningWorkbenchPreferences();
4031
+
2240
4032
  if (tauriMode && toolbar) {
2241
4033
  // In Tauri, controls are migrated to Godot. Keep canvas view only.
2242
4034
  toolbar.style.display = 'none';
@@ -2266,6 +4058,24 @@ window.pathApp = {
2266
4058
  const markBtn = document.getElementById('btn-mark-complete');
2267
4059
  const historyBtn = document.getElementById('btn-toggle-history');
2268
4060
  const closeHistoryBtn = document.getElementById('btn-close-history');
4061
+ const workbenchToggleBtn = document.getElementById('btn-toggle-learning-workbench');
4062
+ const workbenchCloseBtn = document.getElementById('btn-close-learning-workbench');
4063
+ const workbenchRefreshBtn = document.getElementById('btn-refresh-learning-workbench');
4064
+ const workbenchSetQualityBaselineBtn = document.getElementById('btn-set-quality-baseline');
4065
+ const workbenchEvaluateQualityGatesBtn = document.getElementById('btn-evaluate-quality-gates');
4066
+ const workbenchIngestBtn = document.getElementById('btn-ingest-focus-node');
4067
+ const workbenchRunSessionBtn = document.getElementById('btn-run-learning-session');
4068
+ const workbenchRunRetestBtn = document.getElementById('btn-run-retest-session');
4069
+ const workbenchUserIdInput = document.getElementById('learning-user-id');
4070
+ const workbenchActionsList = document.getElementById('learning-session-actions');
4071
+ const workbenchHistoryList = document.getElementById('learning-session-history');
4072
+ const workbenchHistoryKindFilter = document.getElementById('learning-history-kind-filter');
4073
+ const workbenchHistoryFromDate = document.getElementById('learning-history-from-date');
4074
+ const workbenchHistoryToDate = document.getElementById('learning-history-to-date');
4075
+ const workbenchHistoryApplyRangeBtn = document.getElementById('btn-learning-history-apply-range');
4076
+ const workbenchHistoryClearRangeBtn = document.getElementById('btn-learning-history-clear-range');
4077
+ const workbenchHistoryPrevBtn = document.getElementById('btn-learning-history-prev');
4078
+ const workbenchHistoryNextBtn = document.getElementById('btn-learning-history-next');
2269
4079
 
2270
4080
  if (!tauriMode) {
2271
4081
  if (learningModeEl) {
@@ -2286,6 +4096,11 @@ window.pathApp = {
2286
4096
  if (layoutEl) layoutEl.addEventListener('change', () => this.triggerUpdate());
2287
4097
  if (markBtn) markBtn.addEventListener('click', () => this.markComplete());
2288
4098
  if (historyBtn) historyBtn.addEventListener('click', () => this._toggleHistorySidebar());
4099
+ if (workbenchToggleBtn) {
4100
+ workbenchToggleBtn.addEventListener('click', () => {
4101
+ this._toggleLearningWorkbenchSidebar();
4102
+ });
4103
+ }
2289
4104
  }
2290
4105
 
2291
4106
  if (closeHistoryBtn) {
@@ -2299,6 +4114,150 @@ window.pathApp = {
2299
4114
  });
2300
4115
  }
2301
4116
 
4117
+ if (workbenchCloseBtn) {
4118
+ workbenchCloseBtn.addEventListener('click', () => this._closeLearningWorkbenchSidebar());
4119
+ }
4120
+
4121
+ if (workbenchRefreshBtn) {
4122
+ workbenchRefreshBtn.addEventListener('click', () => {
4123
+ void this.refreshLearningWorkbench({ force: true });
4124
+ });
4125
+ }
4126
+
4127
+ if (workbenchSetQualityBaselineBtn) {
4128
+ workbenchSetQualityBaselineBtn.addEventListener('click', () => {
4129
+ void this.setLearningWorkbenchQualityBaselineFromCurrent();
4130
+ });
4131
+ }
4132
+
4133
+ if (workbenchEvaluateQualityGatesBtn) {
4134
+ workbenchEvaluateQualityGatesBtn.addEventListener('click', () => {
4135
+ void this.evaluateLearningWorkbenchQualityGates();
4136
+ });
4137
+ }
4138
+
4139
+ if (workbenchIngestBtn) {
4140
+ workbenchIngestBtn.addEventListener('click', () => {
4141
+ void this.ingestFocusNodeForLearningWorkbench();
4142
+ });
4143
+ }
4144
+
4145
+ if (workbenchRunSessionBtn) {
4146
+ workbenchRunSessionBtn.addEventListener('click', () => {
4147
+ void this.runLearningWorkbenchSession();
4148
+ });
4149
+ }
4150
+
4151
+ if (workbenchRunRetestBtn) {
4152
+ workbenchRunRetestBtn.addEventListener('click', () => {
4153
+ void this.runLearningWorkbenchRetestSession();
4154
+ });
4155
+ }
4156
+
4157
+ if (workbenchUserIdInput) {
4158
+ workbenchUserIdInput.value = this.learningWorkbench.userId || 'path_user_default';
4159
+ workbenchUserIdInput.addEventListener('change', (event) => {
4160
+ const candidate = typeof event?.target?.value === 'string'
4161
+ ? event.target.value
4162
+ : '';
4163
+ this.learningWorkbench.userId = this._normalizeLearningWorkbenchUserId(candidate);
4164
+ const historyQuery = this._getLearningWorkbenchSessionHistoryQuery();
4165
+ this.learningWorkbench.sessionHistoryQuery = {
4166
+ ...historyQuery,
4167
+ offset: 0,
4168
+ };
4169
+ this.learningWorkbench.qualityBaselineSnapshot = this._loadLearningQualityBaseline(this.learningWorkbench.userId);
4170
+ this.learningWorkbench.qualityGateEvaluation = null;
4171
+ workbenchUserIdInput.value = this.learningWorkbench.userId;
4172
+ this._persistLearningWorkbenchPreferences();
4173
+ this._renderLearningWorkbenchState();
4174
+ void this._syncLearningQualityBaselineFromServer(this.learningWorkbench.userId)
4175
+ .then(() => {
4176
+ this._renderLearningWorkbenchState();
4177
+ });
4178
+ });
4179
+ }
4180
+
4181
+ if (workbenchHistoryKindFilter) {
4182
+ workbenchHistoryKindFilter.value = this._getLearningWorkbenchSessionHistoryQuery().executionKind;
4183
+ workbenchHistoryKindFilter.addEventListener('change', (event) => {
4184
+ const nextValue = typeof event?.target?.value === 'string'
4185
+ ? event.target.value
4186
+ : 'all';
4187
+ this._setLearningWorkbenchHistoryExecutionKind(nextValue, { refresh: true });
4188
+ });
4189
+ }
4190
+
4191
+ if (workbenchHistoryFromDate) {
4192
+ workbenchHistoryFromDate.value = this._getLearningWorkbenchSessionHistoryQuery().fromDate;
4193
+ }
4194
+
4195
+ if (workbenchHistoryToDate) {
4196
+ workbenchHistoryToDate.value = this._getLearningWorkbenchSessionHistoryQuery().toDate;
4197
+ }
4198
+
4199
+ if (workbenchHistoryApplyRangeBtn) {
4200
+ workbenchHistoryApplyRangeBtn.addEventListener('click', () => {
4201
+ const fromDate = workbenchHistoryFromDate && typeof workbenchHistoryFromDate.value === 'string'
4202
+ ? workbenchHistoryFromDate.value
4203
+ : '';
4204
+ const toDate = workbenchHistoryToDate && typeof workbenchHistoryToDate.value === 'string'
4205
+ ? workbenchHistoryToDate.value
4206
+ : '';
4207
+ this._applyLearningWorkbenchHistoryDateRange(fromDate, toDate, { refresh: true });
4208
+ });
4209
+ }
4210
+
4211
+ if (workbenchHistoryClearRangeBtn) {
4212
+ workbenchHistoryClearRangeBtn.addEventListener('click', () => {
4213
+ this._applyLearningWorkbenchHistoryDateRange('', '', { refresh: true });
4214
+ });
4215
+ }
4216
+
4217
+ if (workbenchHistoryPrevBtn) {
4218
+ workbenchHistoryPrevBtn.addEventListener('click', () => {
4219
+ this._changeLearningWorkbenchHistoryPage(-1);
4220
+ });
4221
+ }
4222
+
4223
+ if (workbenchHistoryNextBtn) {
4224
+ workbenchHistoryNextBtn.addEventListener('click', () => {
4225
+ this._changeLearningWorkbenchHistoryPage(1);
4226
+ });
4227
+ }
4228
+
4229
+ if (workbenchActionsList) {
4230
+ workbenchActionsList.addEventListener('click', (event) => {
4231
+ const runBtn = event.target && typeof event.target.closest === 'function'
4232
+ ? event.target.closest('.workbench-run-action')
4233
+ : null;
4234
+ if (!runBtn) {
4235
+ return;
4236
+ }
4237
+ const actionKind = decodeURIComponent(runBtn.getAttribute('data-action-kind') || '');
4238
+ const atomId = decodeURIComponent(runBtn.getAttribute('data-atom-id') || '');
4239
+ const source = decodeURIComponent(runBtn.getAttribute('data-action-source') || '');
4240
+ void this.executeLearningWorkbenchAction({
4241
+ actionKind,
4242
+ atomId,
4243
+ source,
4244
+ });
4245
+ });
4246
+ }
4247
+
4248
+ if (workbenchHistoryList) {
4249
+ workbenchHistoryList.addEventListener('click', (event) => {
4250
+ const replayBtn = event.target && typeof event.target.closest === 'function'
4251
+ ? event.target.closest('.workbench-run-history-retest')
4252
+ : null;
4253
+ if (!replayBtn) {
4254
+ return;
4255
+ }
4256
+ const recordId = decodeURIComponent(replayBtn.getAttribute('data-history-record-id') || '');
4257
+ void this.runLearningWorkbenchHistoryRetest(recordId);
4258
+ });
4259
+ }
4260
+
2302
4261
  // Add Target Display UI if missing
2303
4262
  if (!tauriMode && !document.getElementById('target-display') && toolbar && learningModeEl) {
2304
4263
  const targetDiv = document.createElement('div');
@@ -2334,6 +4293,7 @@ window.pathApp = {
2334
4293
 
2335
4294
  this._ensurePathSemanticA11y();
2336
4295
  this._refreshPathSemanticA11y('Path mode initialized');
4296
+ this._renderLearningWorkbenchState();
2337
4297
  },
2338
4298
 
2339
4299
  _ensurePathSemanticA11y: function() {
@@ -2453,6 +4413,14 @@ window.pathApp = {
2453
4413
 
2454
4414
  const now = Date.now();
2455
4415
  if ((now - this.semanticA11yLastAnnouncementAt) < 250) {
4416
+ if (this.semanticA11yDeferredRefreshTimer) {
4417
+ clearTimeout(this.semanticA11yDeferredRefreshTimer);
4418
+ }
4419
+ const delayMs = Math.max(25, 260 - (now - this.semanticA11yLastAnnouncementAt));
4420
+ this.semanticA11yDeferredRefreshTimer = setTimeout(() => {
4421
+ this.semanticA11yDeferredRefreshTimer = null;
4422
+ this._refreshPathSemanticA11y(reason);
4423
+ }, delayMs);
2456
4424
  return;
2457
4425
  }
2458
4426
 
@@ -2932,6 +4900,10 @@ window.pathApp = {
2932
4900
  }
2933
4901
  this.saveHistory();
2934
4902
  this.updateHistorySidebar();
4903
+ const workbenchSidebar = document.getElementById('learning-workbench-sidebar');
4904
+ if (workbenchSidebar && workbenchSidebar.style.display && workbenchSidebar.style.display !== 'none') {
4905
+ void this.refreshLearningWorkbench({ force: false });
4906
+ }
2935
4907
 
2936
4908
  const next = this.nodes.find(n => !this.completedNodes.has(n.id) && n.id !== node.id);
2937
4909
  if (next) setTimeout(() => this.switchCentral(next.id), 500);
@@ -3219,6 +5191,11 @@ window.pathApp = {
3219
5191
  const bridge = (typeof window !== 'undefined') ? window.NoteConnectionRuntime : null;
3220
5192
  const waitForRuntime = this._isTauriMode() && bridge && typeof bridge.whenReady === 'function';
3221
5193
 
5194
+ if (this._isDetachedPreviewWithoutRuntimeBootstrap() && !forceDesktop) {
5195
+ console.log('[PathApp] Detached preview runtime detected; skipping early WebSocket bootstrap.');
5196
+ return;
5197
+ }
5198
+
3222
5199
  if (this._isTauriMode() && !forceDesktop) {
3223
5200
  // In normal Tauri flow, avoid idle early bridge sockets unless the desktop shell explicitly asks for one.
3224
5201
  return;
@@ -3346,7 +5323,12 @@ window.pathApp = {
3346
5323
  // This runs as soon as path_app.js is loaded, before init() is called.
3347
5324
  (function() {
3348
5325
  const isTauri = !!(window.pathApp && typeof window.pathApp._isTauriMode === 'function' && window.pathApp._isTauriMode());
3349
- if (isTauri) {
5326
+ const isDetachedPreview = !!(
5327
+ window.pathApp &&
5328
+ typeof window.pathApp._isDetachedPreviewWithoutRuntimeBootstrap === 'function' &&
5329
+ window.pathApp._isDetachedPreviewWithoutRuntimeBootstrap()
5330
+ );
5331
+ if (isTauri || isDetachedPreview) {
3350
5332
  // Bridge-first Tauri: only connect when Path Mode is explicitly initialized.
3351
5333
  return;
3352
5334
  }
@@ -3358,7 +5340,3 @@ window.pathApp = {
3358
5340
  }
3359
5341
  }, 500);
3360
5342
  })();
3361
-
3362
-
3363
-
3364
-