noteconnection 1.8.0 → 1.9.1

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 (196) hide show
  1. package/README.md +257 -1803
  2. package/dist/src/agent_workspace.contract.parity.test.js +105 -421
  3. package/dist/src/agent_workspace.frontend.test.js +410 -11
  4. package/dist/src/android.knowledgebase.picker.contract.test.js +102 -0
  5. package/dist/src/appimage.patchelf.test.js +113 -0
  6. package/dist/src/appimage.portability.test.js +156 -0
  7. package/dist/src/appimage.update.test.js +127 -0
  8. package/dist/src/backend/FileLoader.js +18 -4
  9. package/dist/src/backend/FileLoader.test.js +125 -0
  10. package/dist/src/backend/GraphBuilder.identity.test.js +86 -0
  11. package/dist/src/backend/GraphBuilder.js +66 -18
  12. package/dist/src/backend/GraphBuilder.matching.test.js +127 -0
  13. package/dist/src/backend/GraphMetrics.js +0 -3
  14. package/dist/src/backend/ResourceIdentity.js +155 -0
  15. package/dist/src/backend/ResourceIdentity.test.js +113 -0
  16. package/dist/src/backend/algorithms/CycleDetection.js +0 -1
  17. package/dist/src/backend/algorithms/HybridEngine.js +0 -3
  18. package/dist/src/backend/algorithms/LayoutEngine.js +0 -1
  19. package/dist/src/backend/algorithms/StatisticalAnalyzer.js +0 -4
  20. package/dist/src/backend/algorithms/TopologicalSort.js +0 -2
  21. package/dist/src/backend/build_dag.js +0 -1
  22. package/dist/src/capacitor.bridge.serialization.contract.test.js +3 -0
  23. package/dist/src/capacitor.runtime.contract.test.js +11 -2
  24. package/dist/src/core/Graph.js +138 -42
  25. package/dist/src/core/Graph.test.js +74 -0
  26. package/dist/src/core/NoteConnection.js +2 -2
  27. package/dist/src/core/PathBridge.js +265 -2
  28. package/dist/src/core/ResourceReference.js +17 -0
  29. package/dist/src/desktop.godot.pack.test.js +142 -0
  30. package/dist/src/desktop.runtime.graph.test.js +66 -0
  31. package/dist/src/export/WorkspaceExportBundle.js +44 -0
  32. package/dist/src/export/WorkspaceExportBundle.test.js +61 -0
  33. package/dist/src/foundation.release.evidence.contract.test.js +127 -9
  34. package/dist/src/foundation.sqlite.runtime.contract.test.js +6 -0
  35. package/dist/src/frontend/README.md +257 -1803
  36. package/dist/src/frontend/agent_workspace.js +665 -7
  37. package/dist/src/frontend/index.html +31 -2
  38. package/dist/src/frontend/knowledge_projection_contract.js +230 -0
  39. package/dist/src/frontend/knowledge_projection_store.js +214 -0
  40. package/dist/src/frontend/libs/path_core.js +158 -42
  41. package/dist/src/frontend/locales/en.json +32 -0
  42. package/dist/src/frontend/locales/zh.json +32 -0
  43. package/dist/src/frontend/mobile_budget_runtime.js +33 -0
  44. package/dist/src/frontend/mobile_exact_analyzer.js +393 -0
  45. package/dist/src/frontend/mobile_identity_contract.js +221 -0
  46. package/dist/src/frontend/mobile_semantic_comparator.js +264 -0
  47. package/dist/src/frontend/path.html +6 -2
  48. package/dist/src/frontend/simulationWorker.js +1 -1
  49. package/dist/src/frontend/source_manager.js +110 -4
  50. package/dist/src/frontend/storage_provider.js +661 -71
  51. package/dist/src/frontend/styles.css +107 -13
  52. package/dist/src/frontend/workspace_panes.js +111 -7
  53. package/dist/src/frontend.runtime.assets.contract.test.js +81 -0
  54. package/dist/src/graph.answer.operator.contract.test.js +72 -0
  55. package/dist/src/index.js +6 -2
  56. package/dist/src/index.knowledge-root.test.js +118 -0
  57. package/dist/src/indexing/IndexLifecycle.js +22 -0
  58. package/dist/src/indexing/IndexLifecycle.test.js +24 -0
  59. package/dist/src/knowledge.api.contract.test.js +4 -2
  60. package/dist/src/knowledge_projection.contract.test.js +134 -0
  61. package/dist/src/knowledge_projection_store.test.js +200 -0
  62. package/dist/src/learning/AnswerQualityEvaluation.js +153 -0
  63. package/dist/src/learning/AnswerQualityEvaluation.test.js +118 -0
  64. package/dist/src/learning/KnowledgeLearningPlatform.js +4687 -3892
  65. package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +418 -0
  66. package/dist/src/learning/KnowledgeLearningPlatform.test.js +243 -6
  67. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +25 -1
  68. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +13 -5
  69. package/dist/src/learning/MeasurementEvidenceConflicts.test.js +78 -0
  70. package/dist/src/learning/PublicAnswerContract.test.js +85 -0
  71. package/dist/src/learning/PublicEvidenceSelection.test.js +94 -0
  72. package/dist/src/learning/PublicMeasurementDisclosure.test.js +54 -0
  73. package/dist/src/learning/agentConversationExecution.js +211 -0
  74. package/dist/src/learning/agentConversationExecution.test.js +209 -0
  75. package/dist/src/learning/agentConversationSerialization.js +266 -0
  76. package/dist/src/learning/agentConversationSerialization.test.js +165 -0
  77. package/dist/src/learning/agentResponseBudget.js +179 -0
  78. package/dist/src/learning/agentResponseBudget.test.js +97 -0
  79. package/dist/src/learning/agentWorkspaceCapabilityContracts.js +65 -0
  80. package/dist/src/learning/answerReleaseReview.js +1191 -250
  81. package/dist/src/learning/answerReleaseReview.test.js +1293 -7
  82. package/dist/src/learning/answerTaskPlan.js +473 -0
  83. package/dist/src/learning/conversationComposer.js +627 -386
  84. package/dist/src/learning/conversationComposer.test.js +1403 -0
  85. package/dist/src/learning/evidenceContextAssembler.js +312 -120
  86. package/dist/src/learning/graphAnswerCoverage.js +37 -0
  87. package/dist/src/learning/graphAnswerCoverage.test.js +93 -0
  88. package/dist/src/learning/graphAnswerCoverageCalibration.js +59 -0
  89. package/dist/src/learning/graphAnswerCoverageCalibration.test.js +46 -0
  90. package/dist/src/learning/graphAnswerFacts.js +94 -0
  91. package/dist/src/learning/graphAnswerFacts.test.js +39 -0
  92. package/dist/src/learning/graphAnswerPlan.js +908 -0
  93. package/dist/src/learning/graphAnswerPlan.test.js +907 -0
  94. package/dist/src/learning/graphAnswerQualityCalibration.js +255 -0
  95. package/dist/src/learning/graphAnswerQualityCalibration.test.js +72 -0
  96. package/dist/src/learning/graphAnswerQualityPolicy.js +30 -0
  97. package/dist/src/learning/graphClaimMatcher.js +104 -0
  98. package/dist/src/learning/graphConditionedContext.js +109 -0
  99. package/dist/src/learning/graphConditionedContext.test.js +99 -0
  100. package/dist/src/learning/graphContextAssembler.js +3 -13
  101. package/dist/src/learning/graphContextAssembler.test.js +2 -2
  102. package/dist/src/learning/graphExpansionPolicy.js +22 -0
  103. package/dist/src/learning/graphExpansionPolicy.test.js +33 -0
  104. package/dist/src/learning/identityMetadata.test.js +26 -0
  105. package/dist/src/learning/index.js +1 -0
  106. package/dist/src/learning/mobileAnswerProjection.js +104 -0
  107. package/dist/src/learning/mobileAnswerProjection.test.js +102 -0
  108. package/dist/src/learning/queryBackend.js +34 -10
  109. package/dist/src/learning/queryBackend.test.js +21 -0
  110. package/dist/src/learning/ragContextPack.js +115 -16
  111. package/dist/src/learning/ragContextPack.test.js +151 -0
  112. package/dist/src/learning/ragEvidenceQuality.js +96 -0
  113. package/dist/src/learning/ragEvidenceQuality.test.js +29 -0
  114. package/dist/src/learning/ragPublicText.js +67 -4
  115. package/dist/src/learning/ragPublicText.test.js +57 -0
  116. package/dist/src/learning/ragSufficiencyJudge.js +11 -1
  117. package/dist/src/learning/ragSufficiencyProviderJudge.js +16 -26
  118. package/dist/src/learning/ragSufficiencyProviderJudge.test.js +51 -0
  119. package/dist/src/learning/requestNormalization.js +106 -0
  120. package/dist/src/learning/requestNormalization.test.js +101 -0
  121. package/dist/src/learning/store.js +116 -12
  122. package/dist/src/learning/store.test.js +299 -0
  123. package/dist/src/learning/vectorAccelerationAdapter.js +22 -6
  124. package/dist/src/learning/vectorAccelerationAdapter.test.js +30 -0
  125. package/dist/src/learning/workspaceHydration.js +168 -0
  126. package/dist/src/learning/workspaceHydration.test.js +30 -0
  127. package/dist/src/license.policy.contract.test.js +9 -4
  128. package/dist/src/markdown/MarkdownGateway.js +4 -0
  129. package/dist/src/markdown/MarkdownGateway.test.js +59 -0
  130. package/dist/src/middleware/SseResponseWriter.js +122 -0
  131. package/dist/src/middleware/SseResponseWriter.test.js +54 -0
  132. package/dist/src/middleware/auth.js +28 -7
  133. package/dist/src/middleware/auth.test.js +33 -0
  134. package/dist/src/middleware/index.js +2 -1
  135. package/dist/src/mobile.android.evidence.contract.test.js +146 -0
  136. package/dist/src/mobile.android.signing.contract.test.js +66 -0
  137. package/dist/src/mobile.artifact.contract.test.js +144 -0
  138. package/dist/src/mobile.budget.contract.test.js +90 -0
  139. package/dist/src/mobile.canonical-id.readiness.test.js +54 -0
  140. package/dist/src/mobile.graph.parity.contract.test.js +207 -0
  141. package/dist/src/mobile.identity.contract.test.js +66 -0
  142. package/dist/src/mobile.identity.corpus.manifest.test.js +72 -0
  143. package/dist/src/mobile.identity.corpus.test.js +40 -0
  144. package/dist/src/mobile.native.recovery.contract.test.js +64 -0
  145. package/dist/src/mobile.pipeline.test.js +11 -1
  146. package/dist/src/mobile.profile.contract.test.js +225 -0
  147. package/dist/src/mobile.semantic.parity.test.js +92 -0
  148. package/dist/src/mobile_exact_analyzer.test.js +113 -0
  149. package/dist/src/notemd/ContentGenerator.js +4 -9
  150. package/dist/src/notemd/FileProcessor.js +4 -9
  151. package/dist/src/notemd/NotemdService.js +12 -17
  152. package/dist/src/notemd/Translator.js +4 -9
  153. package/dist/src/notemd/progressReporter.js +9 -0
  154. package/dist/src/notemd.server.integration.test.js +119 -4
  155. package/dist/src/notemd.workflow.pipeline.test.js +1 -1
  156. package/dist/src/pathbridge.handshake.contract.test.js +29 -0
  157. package/dist/src/pathbridge.host-adapter.contract.test.js +133 -0
  158. package/dist/src/pathcore.bundle.contract.test.js +8 -0
  159. package/dist/src/platform/ExportProfile.js +28 -0
  160. package/dist/src/platform/MobileBudget.js +11 -0
  161. package/dist/src/platform/PlatformCapabilities.js +14 -0
  162. package/dist/src/platform/PlatformCapabilities.test.js +6 -0
  163. package/dist/src/query_backend.external_http.integration.test.js +2 -2
  164. package/dist/src/release.godot.mirror.contract.test.js +28 -4
  165. package/dist/src/resources/ResourceRegistry.js +40 -0
  166. package/dist/src/resources/ResourceRegistry.test.js +36 -0
  167. package/dist/src/routes/index.js +0 -4
  168. package/dist/src/routes/knowledge.js +46 -33
  169. package/dist/src/routes/knowledgePayload.js +185 -0
  170. package/dist/src/routes/knowledgePayload.test.js +53 -0
  171. package/dist/src/routes/markdown.js +8 -0
  172. package/dist/src/routes/notemd.js +990 -234
  173. package/dist/src/routes/registry.contract.test.js +5 -2
  174. package/dist/src/routes/registry.shadow.contract.test.js +22 -0
  175. package/dist/src/routes/render.js +14 -51
  176. package/dist/src/runtime.capabilities.test.js +18 -2
  177. package/dist/src/server.js +566 -1090
  178. package/dist/src/server.migration.test.js +417 -15
  179. package/dist/src/sidecar.build.lock.contract.test.js +201 -0
  180. package/dist/src/sidecar.freshness.contract.test.js +184 -0
  181. package/dist/src/sidecar.replacement.boundary.contract.test.js +6 -3
  182. package/dist/src/sidecar.supply.readiness.contract.test.js +11 -0
  183. package/dist/src/simulation.worker.offline.test.js +104 -0
  184. package/dist/src/storage.provider.capacitor.content.contract.test.js +4 -1
  185. package/dist/src/storage.provider.contract.test.js +152 -1
  186. package/dist/src/tauri.frontend.build.contract.test.js +8 -0
  187. package/dist/src/tauri.window.evidence.execution.test.js +68 -0
  188. package/dist/src/utils/RuntimePaths.js +6 -0
  189. package/dist/src/utils/RuntimePaths.test.js +27 -0
  190. package/dist/src/windows.installer.build.test.js +109 -0
  191. package/dist/src/windows.installer.payload.test.js +76 -0
  192. package/dist/src/windows.installer.runtime.test.js +69 -0
  193. package/dist/src/workspace/WorkspaceRegistry.js +13 -0
  194. package/package.json +32 -10
  195. package/dist/src/routes/data.js +0 -267
  196. package/dist/src/routes/diagnostics.js +0 -51
@@ -0,0 +1,86 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const GraphBuilder_1 = require("./GraphBuilder");
4
+ const config_1 = require("./config");
5
+ describe('GraphBuilder identity compatibility', () => {
6
+ const originalConfig = {
7
+ ...config_1.config,
8
+ exclusionList: [...config_1.config.exclusionList],
9
+ };
10
+ beforeEach(() => {
11
+ config_1.config.enableTags = false;
12
+ config_1.config.enableStatisticalInference = false;
13
+ config_1.config.enableVectorSimilarity = false;
14
+ config_1.config.enableHybridInference = false;
15
+ config_1.config.enableGPU = false;
16
+ config_1.config.enableGPULayout = false;
17
+ config_1.config.clusteringStrategy = 'folder';
18
+ config_1.config.exclusionList = [];
19
+ });
20
+ afterEach(() => {
21
+ Object.assign(config_1.config, originalConfig);
22
+ });
23
+ test('preserves identity metadata, reads URI frontmatter, and accepts URI layouts', async () => {
24
+ const files = [
25
+ {
26
+ filepath: 'workspace/algebra/a.md',
27
+ filename: 'A',
28
+ content: '# A',
29
+ relativePath: 'algebra/a.md',
30
+ sourceUri: 'note://workspace/v1/algebra/a.md',
31
+ canonicalId: 'algebra/a',
32
+ revision: 'sha256:a',
33
+ identityAliases: ['A', 'algebra/a.md'],
34
+ },
35
+ {
36
+ filepath: 'workspace/algebra/b.md',
37
+ filename: 'B',
38
+ content: '---\nprerequisites:\n - note://workspace/v1/algebra/a.md\n---\n# B',
39
+ relativePath: 'algebra/b.md',
40
+ sourceUri: 'note://workspace/v1/algebra/b.md',
41
+ revision: 'sha256:b',
42
+ identityAliases: ['B', 'algebra/b.md'],
43
+ },
44
+ ];
45
+ const graph = await GraphBuilder_1.GraphBuilder.build(files, new Map([
46
+ ['note://workspace/v1/algebra/a.md', { x: 12, y: 34 }],
47
+ ]));
48
+ expect(graph.getNode('A')).toEqual(expect.objectContaining({
49
+ sourceUri: 'note://workspace/v1/algebra/a.md',
50
+ canonicalId: 'algebra/a',
51
+ revision: 'sha256:a',
52
+ identityAliases: expect.arrayContaining(['algebra/a.md']),
53
+ x: 12,
54
+ y: 34,
55
+ }));
56
+ expect(graph.getIncomingEdges('B')).toEqual(expect.arrayContaining([
57
+ expect.objectContaining({ source: 'A', target: 'B', type: 'explicit-prerequisite' }),
58
+ ]));
59
+ });
60
+ test('uses indexed keyword candidates without changing exact-match semantics', async () => {
61
+ const files = [
62
+ {
63
+ filepath: 'workspace/alpha.md',
64
+ filename: 'Alpha',
65
+ content: 'Alpha is a prerequisite concept.',
66
+ },
67
+ {
68
+ filepath: 'workspace/context.md',
69
+ filename: 'Context',
70
+ content: 'This context mentions Alpha as an explicit phrase.',
71
+ },
72
+ {
73
+ filepath: 'workspace/beta.md',
74
+ filename: 'Beta',
75
+ content: 'This context mentions alphabet but not the standalone concept.',
76
+ },
77
+ ];
78
+ const graph = await GraphBuilder_1.GraphBuilder.build(files);
79
+ expect(graph.getOutgoingEdges('Alpha')).toEqual(expect.arrayContaining([
80
+ expect.objectContaining({ target: 'Context', type: 'keyword-match' }),
81
+ ]));
82
+ expect(graph.getOutgoingEdges('Alpha')).not.toEqual(expect.arrayContaining([
83
+ expect.objectContaining({ target: 'Beta', type: 'keyword-match' }),
84
+ ]));
85
+ });
86
+ });
@@ -52,11 +52,34 @@ const HybridEngine_1 = require("./algorithms/HybridEngine");
52
52
  const PerformanceLogger_1 = require("./utils/PerformanceLogger");
53
53
  const LayoutEngine_1 = require("./algorithms/LayoutEngine");
54
54
  const WorkerRuntime_1 = require("./utils/WorkerRuntime");
55
+ const ResourceIdentity_1 = require("./ResourceIdentity");
55
56
  /**
56
57
  * Service to build the graph from raw files.
57
58
  * 从原始文件构建图的服务。
58
59
  */
59
60
  class GraphBuilder {
61
+ static findLayoutPosition(file, layout) {
62
+ if (!layout)
63
+ return undefined;
64
+ const references = [
65
+ file.sourceUri,
66
+ file.relativePath,
67
+ ...(file.identityAliases ?? []),
68
+ file.filename,
69
+ ].filter((reference) => Boolean(reference));
70
+ for (const reference of references) {
71
+ const exactPosition = layout.get(reference);
72
+ if (exactPosition)
73
+ return exactPosition;
74
+ }
75
+ const normalizedReferences = new Set(references.map(ResourceIdentity_1.normalizeResourceReference));
76
+ for (const [reference, position] of layout.entries()) {
77
+ if (normalizedReferences.has((0, ResourceIdentity_1.normalizeResourceReference)(reference))) {
78
+ return position;
79
+ }
80
+ }
81
+ return undefined;
82
+ }
60
83
  /**
61
84
  * Builds a graph from raw files using keyword matching.
62
85
  * 使用关键词匹配从原始文件构建图。
@@ -65,13 +88,13 @@ class GraphBuilder {
65
88
  */
66
89
  static async build(files, layout) {
67
90
  PerformanceLogger_1.PerformanceLogger.logSystemInfo();
91
+ (0, ResourceIdentity_1.assertUniqueLegacyResourceIds)(files);
68
92
  const graph = new Graph_1.Graph();
69
93
  // 1. Add all nodes first
70
94
  PerformanceLogger_1.PerformanceLogger.start('Node Initialization');
71
95
  // 1. 首先添加所有节点
72
96
  const fileMap = new Map();
73
97
  files.forEach(file => {
74
- // Parse Metadata (Tags, Prerequisites, Next)
75
98
  const metadata = frontmatterParser_1.FrontmatterParser.parse(file.content);
76
99
  const node = {
77
100
  id: file.filename,
@@ -79,21 +102,28 @@ class GraphBuilder {
79
102
  inDegree: 0,
80
103
  outDegree: 0,
81
104
  content: file.content,
105
+ sourceUri: file.sourceUri,
106
+ canonicalId: file.canonicalId,
107
+ revision: file.revision,
108
+ identityAliases: file.identityAliases,
82
109
  metadata: {
83
110
  filepath: file.filepath,
111
+ relativePath: file.relativePath,
112
+ sourceUri: file.sourceUri,
113
+ revision: file.revision,
84
114
  tags: metadata.tags,
85
115
  prerequisites: metadata.prerequisites,
86
116
  next: metadata.next
87
117
  }
88
118
  };
89
- if (layout && layout.has(file.filename)) {
90
- const pos = layout.get(file.filename);
119
+ const savedPosition = this.findLayoutPosition(file, layout);
120
+ if (savedPosition) {
121
+ const pos = savedPosition;
91
122
  node.x = pos.x;
92
123
  node.y = pos.y;
93
124
  }
94
125
  graph.addNode(node);
95
126
  fileMap.set(file.filename, file);
96
- // 1b. Add Tag Nodes
97
127
  if (config_1.config.enableTags) {
98
128
  metadata.tags.forEach(tag => {
99
129
  const tagId = `#${tag}`;
@@ -105,7 +135,6 @@ class GraphBuilder {
105
135
  clusterId: 'tags' // Group tags together
106
136
  });
107
137
  }
108
- // Edge: Note -> Tag
109
138
  graph.addEdge(node.id, tagId, 'tagged');
110
139
  });
111
140
  }
@@ -209,7 +238,6 @@ class GraphBuilder {
209
238
  const similar = sharedVectorSpace.getSimilar(file.filename, 3); // Top 3 similar
210
239
  similar.forEach(sim => {
211
240
  if (sim.score > 0.3) { // Threshold
212
- // Add UNDIRECTED association
213
241
  graph.addEdge(file.filename, sim.id, 'vector-association', sim.score);
214
242
  similarityEdges++;
215
243
  }
@@ -369,6 +397,7 @@ class GraphBuilder {
369
397
  const chunkSize = Math.ceil(files.length / workerCount);
370
398
  const targetIds = files.map(f => f.filename);
371
399
  const workerPromises = [];
400
+ const matchingWorkers = [];
372
401
  const workerRuntime = (0, WorkerRuntime_1.resolveWorkerRuntimePath)(__dirname, 'workers/keywordMatchWorker.ts');
373
402
  const actualWorkerPath = workerRuntime.workerPath;
374
403
  const isTsNode = workerRuntime.isTsNode;
@@ -409,6 +438,7 @@ class GraphBuilder {
409
438
  workerData: workerPayload,
410
439
  execArgv
411
440
  });
441
+ matchingWorkers.push(worker);
412
442
  worker.on('message', (results) => {
413
443
  results.forEach(res => {
414
444
  graph.addEdge(res.target, res.source, 'keyword-match');
@@ -441,24 +471,42 @@ class GraphBuilder {
441
471
  }
442
472
  catch (err) {
443
473
  console.error('[GraphBuilder] Parallel matching failed, falling back to sequential.', err);
444
- // Fallback
474
+ // No worker may append edges while downstream graph analysis sees the fallback.
475
+ await Promise.allSettled(matchingWorkers.map(worker => worker.terminate()));
476
+ await Promise.allSettled(workerPromises);
445
477
  this.runSequentialMatching(files, graph);
446
478
  }
447
479
  }
448
480
  static runSequentialMatching(files, graph) {
449
- files.forEach(sourceFile => {
450
- const sourceId = sourceFile.filename;
451
- const content = sourceFile.content;
452
- files.forEach(targetFile => {
453
- const targetId = targetFile.filename;
454
- if (sourceId === targetId)
455
- return; // Skip self | 跳过自身
456
- // Exclusion Check
457
- if (config_1.config.exclusionList.includes(targetId)) {
481
+ // Exact-phrase matching uses JavaScript's ASCII word boundaries. Index
482
+ // those same word runs so mixed CJK/ASCII text cannot hide a valid match.
483
+ // Substring matching requires a full candidate set; token equality is not
484
+ // a conservative prefilter for fuzzy matches inside a longer word.
485
+ const candidatesByAnchor = new Map();
486
+ files.forEach((sourceFile) => {
487
+ const tokens = sourceFile.content.toLowerCase().match(/[A-Za-z0-9_]+/g) || [];
488
+ const seenTokens = new Set(tokens);
489
+ seenTokens.forEach((token) => {
490
+ const candidates = candidatesByAnchor.get(token) || [];
491
+ candidates.push(sourceFile);
492
+ candidatesByAnchor.set(token, candidates);
493
+ });
494
+ });
495
+ files.forEach((targetFile) => {
496
+ const targetId = targetFile.filename;
497
+ if (config_1.config.exclusionList.includes(targetId)) {
498
+ return;
499
+ }
500
+ const anchor = targetId.toLowerCase().match(/[A-Za-z0-9_]+/)?.[0] || '';
501
+ const candidateSources = config_1.config.matchingStrategy === 'exact-phrase' && anchor
502
+ ? (candidatesByAnchor.get(anchor) || [])
503
+ : files;
504
+ candidateSources.forEach((sourceFile) => {
505
+ const sourceId = sourceFile.filename;
506
+ if (sourceId === targetId) {
458
507
  return;
459
508
  }
460
- if ((0, stringUtils_1.checkMatch)(content, targetId, config_1.config.matchingStrategy)) {
461
- // Found a reference!
509
+ if ((0, stringUtils_1.checkMatch)(sourceFile.content, targetId, config_1.config.matchingStrategy)) {
462
510
  // Target (Concept) -> Source (Context)
463
511
  graph.addEdge(targetId, sourceId, 'keyword-match');
464
512
  }
@@ -0,0 +1,127 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ var __importDefault = (this && this.__importDefault) || function (mod) {
36
+ return (mod && mod.__esModule) ? mod : { "default": mod };
37
+ };
38
+ Object.defineProperty(exports, "__esModule", { value: true });
39
+ const fs = __importStar(require("fs"));
40
+ const os = __importStar(require("os"));
41
+ const path = __importStar(require("path"));
42
+ const GraphBuilder_1 = require("./GraphBuilder");
43
+ const config_1 = require("./config");
44
+ const stringUtils_1 = require("./utils/stringUtils");
45
+ const worker_threads_1 = __importDefault(require("worker_threads"));
46
+ const events_1 = require("events");
47
+ describe('graph keyword matching equivalence', () => {
48
+ const original = { ...config_1.config, exclusionList: [...config_1.config.exclusionList] };
49
+ let root;
50
+ beforeEach(() => {
51
+ root = fs.mkdtempSync(path.join(os.tmpdir(), 'graph-matching-'));
52
+ Object.assign(config_1.config, {
53
+ enableTags: false, enableStatisticalInference: false, enableVectorSimilarity: false,
54
+ enableHybridInference: false, enableGPU: false, enableGPULayout: false,
55
+ clusteringStrategy: 'folder', exclusionList: ['ignored'], maxWorkers: 1,
56
+ });
57
+ jest.spyOn(console, 'log').mockImplementation(() => undefined);
58
+ });
59
+ afterEach(() => {
60
+ Object.assign(config_1.config, original);
61
+ jest.restoreAllMocks();
62
+ fs.rmSync(root, { recursive: true, force: true });
63
+ });
64
+ function corpus() {
65
+ const records = [
66
+ ['cat', ''], ['知识', ''], ['Alpha', ''], ['++', ''], ['café', ''],
67
+ ['ignored', ''], ['self', 'self'],
68
+ ['english-context', 'concatenate'], ['chinese-context', '知识图谱'],
69
+ ['mixed-context', 'Alpha知识'], ['punctuation-context', 'a++b'],
70
+ ['unicode-context', 'précaféiné'], ['excluded-context', 'ignored'],
71
+ ];
72
+ return records.map(([filename, content]) => {
73
+ const filepath = path.join(root, filename + '.md');
74
+ fs.writeFileSync(filepath, content);
75
+ return { filename, filepath, content };
76
+ });
77
+ }
78
+ function expectedEdges(files) {
79
+ return files.flatMap(source => files
80
+ .filter(target => source.filename !== target.filename
81
+ && !config_1.config.exclusionList.includes(target.filename)
82
+ && (0, stringUtils_1.checkMatch)(source.content, target.filename, config_1.config.matchingStrategy))
83
+ .map(target => target.filename + '->' + source.filename)).sort();
84
+ }
85
+ test.each(['exact-phrase', 'fuzzy'])('sequential matching preserves %s semantics', async (strategy) => {
86
+ config_1.config.matchingStrategy = strategy;
87
+ const files = corpus();
88
+ const graph = await GraphBuilder_1.GraphBuilder.build(files);
89
+ expect(graph.getEdges().filter(edge => edge.type === 'keyword-match')
90
+ .map(edge => edge.source + '->' + edge.target).sort()).toEqual(expectedEdges(files));
91
+ });
92
+ test.each([['exact-phrase', true], ['exact-phrase', false], ['fuzzy', true], ['fuzzy', false]])('worker matching preserves %s semantics with memorySavingMode=%s', async (strategy, memorySavingMode) => {
93
+ config_1.config.matchingStrategy = strategy;
94
+ config_1.config.memorySavingMode = memorySavingMode;
95
+ const files = corpus();
96
+ for (let index = 0; files.length <= 200; index++) {
97
+ const filename = 'unused-' + index;
98
+ const filepath = path.join(root, filename + '.md');
99
+ fs.writeFileSync(filepath, '');
100
+ files.push({ filename, filepath, content: '' });
101
+ }
102
+ const graph = await GraphBuilder_1.GraphBuilder.build(files);
103
+ expect(graph.getEdges().filter(edge => edge.type === 'keyword-match')
104
+ .map(edge => edge.source + '->' + edge.target).sort()).toEqual(expectedEdges(files));
105
+ }, 30000);
106
+ test('quiesces all workers before publishing sequential fallback after a worker failure', async () => {
107
+ config_1.config.matchingStrategy = 'fuzzy';
108
+ config_1.config.maxWorkers = 2;
109
+ const files = corpus();
110
+ for (let index = 0; files.length <= 200; index++)
111
+ files.push({ filename: 'padding-' + index, filepath: path.join(root, 'padding-' + index + '.md'), content: '' });
112
+ const children = [];
113
+ jest.spyOn(console, 'error').mockImplementation(() => undefined);
114
+ jest.spyOn(worker_threads_1.default, 'Worker').mockImplementation(() => {
115
+ const child = Object.assign(new events_1.EventEmitter(), { terminate: jest.fn(async () => { child.emit('exit', 0); return 0; }) });
116
+ children.push(child);
117
+ if (children.length === 1)
118
+ setImmediate(() => child.emit('error', new Error('injected worker failure')));
119
+ return child;
120
+ });
121
+ const graph = await GraphBuilder_1.GraphBuilder.build(files);
122
+ expect(graph.getEdges().filter(edge => edge.type === 'keyword-match').map(edge => edge.source + '->' + edge.target).sort()).toEqual(expectedEdges(files));
123
+ expect(children).toHaveLength(2);
124
+ for (const child of children)
125
+ expect(child.terminate).toHaveBeenCalledTimes(1);
126
+ });
127
+ });
@@ -235,8 +235,6 @@ class GraphMetrics {
235
235
  }
236
236
  try {
237
237
  const results = await Promise.all(workerPromises);
238
- // Merge results
239
- // 合并结果
240
238
  const totalCB = new Map();
241
239
  nodes.forEach(n => totalCB.set(n.id, 0));
242
240
  results.forEach(partial => {
@@ -261,7 +259,6 @@ class GraphMetrics {
261
259
  static calculateBetweenness(graph) {
262
260
  const nodes = graph.toJSON().nodes;
263
261
  const cb = new Map();
264
- // Initialize
265
262
  nodes.forEach(n => cb.set(n.id, 0));
266
263
  // For each node s, calculate dependencies
267
264
  nodes.forEach(sNode => {
@@ -0,0 +1,155 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ exports.normalizeResourceReference = void 0;
37
+ exports.normalizePortableResourcePath = normalizePortableResourcePath;
38
+ exports.createResourceIdentity = createResourceIdentity;
39
+ exports.normalizeResourceRelativePath = normalizeResourceRelativePath;
40
+ exports.assertUniqueLegacyResourceIds = assertUniqueLegacyResourceIds;
41
+ const path = __importStar(require("path"));
42
+ const crypto_1 = require("crypto");
43
+ const ResourceReference_1 = require("../core/ResourceReference");
44
+ var ResourceReference_2 = require("../core/ResourceReference");
45
+ Object.defineProperty(exports, "normalizeResourceReference", { enumerable: true, get: function () { return ResourceReference_2.normalizeResourceReference; } });
46
+ const SOURCE_URI_PREFIX = 'note://workspace/v1/';
47
+ function normalizeUnicodeText(value, fieldName) {
48
+ if (typeof value !== 'string') {
49
+ throw new Error(`${fieldName} must be a string`);
50
+ }
51
+ if (value.includes('\0')) {
52
+ throw new Error(`${fieldName} must not contain NUL characters`);
53
+ }
54
+ return value.normalize('NFC');
55
+ }
56
+ /**
57
+ * Produces the case-folded path key used by portable aliases and source URIs.
58
+ * The lower-case policy is deliberate: a workspace must not change identity when
59
+ * moved between Windows and a case-sensitive POSIX host.
60
+ */
61
+ function normalizePortableResourcePath(relativePath) {
62
+ const normalized = normalizeUnicodeText(relativePath, 'Relative resource path')
63
+ .replace(/\\/g, '/');
64
+ if (!normalized
65
+ || normalized.startsWith('/')
66
+ || /^[A-Za-z]:\//.test(normalized)) {
67
+ throw new Error(`Relative resource path must stay inside the workspace: ${relativePath}`);
68
+ }
69
+ const segments = normalized.split('/');
70
+ if (segments.some(segment => !segment || segment === '..')) {
71
+ throw new Error(`Relative resource path contains an unsafe segment: ${relativePath}`);
72
+ }
73
+ return segments
74
+ .filter(segment => segment !== '.')
75
+ .map(segment => segment.toLowerCase())
76
+ .join('/');
77
+ }
78
+ function createSourceUri(canonicalRelativePath) {
79
+ const encodedSegments = canonicalRelativePath
80
+ .split('/')
81
+ .map(segment => encodeURIComponent(segment));
82
+ return `${SOURCE_URI_PREFIX}${encodedSegments.join('/')}`;
83
+ }
84
+ function createCanonicalId(canonicalRelativePath) {
85
+ return canonicalRelativePath.replace(/\.(?:md|markdown)$/i, '');
86
+ }
87
+ function createContentRevision(content) {
88
+ const normalizedContent = normalizeUnicodeText(content, 'Resource content');
89
+ return `sha256:${(0, crypto_1.createHash)('sha256').update(normalizedContent, 'utf8').digest('hex')}`;
90
+ }
91
+ /**
92
+ * Builds additive identity metadata for a loaded Markdown resource.
93
+ * `relativePath` keeps its display spelling in aliases while the source URI is
94
+ * canonicalized for cross-platform persistence.
95
+ */
96
+ function createResourceIdentity(relativePath, legacyId, content) {
97
+ const displayRelativePath = normalizeUnicodeText(relativePath, 'Relative resource path')
98
+ .replace(/\\/g, '/');
99
+ const canonicalRelativePath = normalizePortableResourcePath(displayRelativePath);
100
+ const normalizedLegacyId = normalizeUnicodeText(legacyId, 'Legacy resource ID');
101
+ if (!normalizedLegacyId.trim()) {
102
+ throw new Error('Legacy resource ID must not be empty');
103
+ }
104
+ const identityAliases = Array.from(new Set([
105
+ normalizedLegacyId,
106
+ normalizedLegacyId.toLowerCase().endsWith('.md') ? normalizedLegacyId : `${normalizedLegacyId}.md`,
107
+ displayRelativePath,
108
+ canonicalRelativePath,
109
+ ]));
110
+ return {
111
+ sourceUri: createSourceUri(canonicalRelativePath),
112
+ canonicalId: createCanonicalId(canonicalRelativePath),
113
+ revision: createContentRevision(content),
114
+ identityAliases,
115
+ };
116
+ }
117
+ /**
118
+ * Returns the stable, workspace-relative spelling used by persistence and diagnostics.
119
+ * The legacy basename remains a compatibility alias; this value is the migration input
120
+ * for a future stable source URI.
121
+ */
122
+ function normalizeResourceRelativePath(rootPath, filePath) {
123
+ normalizeUnicodeText(rootPath, 'Workspace root');
124
+ normalizeUnicodeText(filePath, 'Resource path');
125
+ const resolvedRoot = path.resolve(rootPath);
126
+ const resolvedFile = path.resolve(filePath);
127
+ const relativePath = path.relative(resolvedRoot, resolvedFile);
128
+ const parentPrefix = `..${path.sep}`;
129
+ if (!relativePath
130
+ || relativePath === '..'
131
+ || relativePath.startsWith(parentPrefix)
132
+ || path.isAbsolute(relativePath)) {
133
+ throw new Error(`Resource path is outside workspace root: ${filePath}`);
134
+ }
135
+ return relativePath.split(path.sep).join('/').normalize('NFC');
136
+ }
137
+ /**
138
+ * Prevents the current basename-based graph identity from silently dropping files.
139
+ * This guard is intentionally fail-fast until stable source IDs and aliases are migrated.
140
+ */
141
+ function assertUniqueLegacyResourceIds(files) {
142
+ const pathsById = new Map();
143
+ files.forEach((file) => {
144
+ const identityKey = (0, ResourceReference_1.normalizeResourceReference)(file.filename);
145
+ const paths = pathsById.get(identityKey) ?? [];
146
+ paths.push(file.filepath);
147
+ pathsById.set(identityKey, paths);
148
+ });
149
+ const collisions = Array.from(pathsById.entries())
150
+ .filter(([, paths]) => paths.length > 1)
151
+ .map(([id, paths]) => `${id}: ${paths.join(', ')}`);
152
+ if (collisions.length > 0) {
153
+ throw new Error(`Ambiguous legacy resource identity detected; duplicate basenames must be resolved before graph construction: ${collisions.join('; ')}`);
154
+ }
155
+ }
@@ -0,0 +1,113 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
14
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
15
+ }) : function(o, v) {
16
+ o["default"] = v;
17
+ });
18
+ var __importStar = (this && this.__importStar) || (function () {
19
+ var ownKeys = function(o) {
20
+ ownKeys = Object.getOwnPropertyNames || function (o) {
21
+ var ar = [];
22
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
23
+ return ar;
24
+ };
25
+ return ownKeys(o);
26
+ };
27
+ return function (mod) {
28
+ if (mod && mod.__esModule) return mod;
29
+ var result = {};
30
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
31
+ __setModuleDefault(result, mod);
32
+ return result;
33
+ };
34
+ })();
35
+ Object.defineProperty(exports, "__esModule", { value: true });
36
+ const path = __importStar(require("path"));
37
+ const ResourceIdentity_1 = require("./ResourceIdentity");
38
+ describe('Resource identity boundary', () => {
39
+ test('normalizes workspace-relative paths to POSIX separators', () => {
40
+ const root = path.resolve('workspace', 'Knowledge_Base');
41
+ const filePath = path.join(root, 'algebra', 'index.md');
42
+ expect((0, ResourceIdentity_1.normalizeResourceRelativePath)(root, filePath)).toBe('algebra/index.md');
43
+ });
44
+ test('rejects a path outside the workspace root', () => {
45
+ const root = path.resolve('workspace', 'Knowledge_Base');
46
+ expect(() => (0, ResourceIdentity_1.normalizeResourceRelativePath)(root, path.resolve(root, '..', 'other.md')))
47
+ .toThrow(/outside workspace root/i);
48
+ });
49
+ test('fails before graph construction when legacy basenames collide', () => {
50
+ expect(() => (0, ResourceIdentity_1.assertUniqueLegacyResourceIds)([
51
+ { filename: 'index', filepath: 'algebra/index.md' },
52
+ { filename: 'index', filepath: 'physics/index.md' },
53
+ ])).toThrow(/index.*algebra\/index\.md.*physics\/index\.md/i);
54
+ });
55
+ test('accepts unique legacy IDs', () => {
56
+ expect(() => (0, ResourceIdentity_1.assertUniqueLegacyResourceIds)([
57
+ { filename: 'algebra-index', filepath: 'algebra/index.md' },
58
+ { filename: 'physics-index', filepath: 'physics/index.md' },
59
+ ])).not.toThrow();
60
+ });
61
+ test('builds a versioned portable source URI and stable content revision', () => {
62
+ const identity = (0, ResourceIdentity_1.createResourceIdentity)('Algebra Notes\\Index #1.MD', 'Index #1', '# Algebra');
63
+ expect(identity.sourceUri).toBe('note://workspace/v1/algebra%20notes/index%20%231.md');
64
+ expect(identity.canonicalId).toBe('algebra notes/index #1');
65
+ expect(identity.revision).toMatch(/^sha256:[0-9a-f]{64}$/);
66
+ expect(identity.identityAliases).toEqual(expect.arrayContaining([
67
+ 'Index #1',
68
+ 'Algebra Notes/Index #1.MD',
69
+ 'algebra notes/index #1.md',
70
+ ]));
71
+ });
72
+ test('keeps the revision stable for equal content and changes it for different content', () => {
73
+ const first = (0, ResourceIdentity_1.createResourceIdentity)('notes/a.md', 'a', 'same');
74
+ const same = (0, ResourceIdentity_1.createResourceIdentity)('notes/a.md', 'a', 'same');
75
+ const changed = (0, ResourceIdentity_1.createResourceIdentity)('notes/a.md', 'a', 'changed');
76
+ expect(same.revision).toBe(first.revision);
77
+ expect(changed.revision).not.toBe(first.revision);
78
+ });
79
+ test('rejects unsafe relative source paths', () => {
80
+ expect(() => (0, ResourceIdentity_1.createResourceIdentity)('../outside.md', 'outside', 'content'))
81
+ .toThrow(/relative resource path/i);
82
+ expect(() => (0, ResourceIdentity_1.createResourceIdentity)('notes/\0.md', 'nul', 'content'))
83
+ .toThrow(/NUL/i);
84
+ });
85
+ test('rejects case-folded legacy collisions for cross-platform determinism', () => {
86
+ expect(() => (0, ResourceIdentity_1.assertUniqueLegacyResourceIds)([
87
+ { filename: 'Index', filepath: 'Algebra/Index.md' },
88
+ { filename: 'index', filepath: 'Physics/index.md' },
89
+ ])).toThrow(/case-insensitive|ambiguous/i);
90
+ });
91
+ test('keeps same-content documents distinct by source URI while sharing the revision', () => {
92
+ const first = (0, ResourceIdentity_1.createResourceIdentity)('notes/first.md', 'First', 'same body');
93
+ const second = (0, ResourceIdentity_1.createResourceIdentity)('notes/second.md', 'Second', 'same body');
94
+ expect(first.revision).toBe(second.revision);
95
+ expect(first.sourceUri).not.toBe(second.sourceUri);
96
+ expect(first.identityAliases).toEqual(expect.arrayContaining(['First', 'notes/first.md']));
97
+ expect(second.identityAliases).toEqual(expect.arrayContaining(['Second', 'notes/second.md']));
98
+ });
99
+ test('treats move and rename as an additive alias transition before any canonical cutover', () => {
100
+ const before = (0, ResourceIdentity_1.createResourceIdentity)('algebra/intro.md', 'Intro', 'body');
101
+ const after = (0, ResourceIdentity_1.createResourceIdentity)('foundations/start.md', 'Intro', 'body');
102
+ expect(after.revision).toBe(before.revision);
103
+ expect(after.sourceUri).not.toBe(before.sourceUri);
104
+ expect(before.identityAliases).toContain('algebra/intro.md');
105
+ expect(after.identityAliases).toContain('foundations/start.md');
106
+ });
107
+ test('rejects Unicode-normalized collisions before graph mutation', () => {
108
+ expect(() => (0, ResourceIdentity_1.assertUniqueLegacyResourceIds)([
109
+ { filename: 'Cafe\u0301', filepath: 'one/Cafe\u0301.md' },
110
+ { filename: 'Caf\u00e9', filepath: 'two/Caf\u00e9.md' },
111
+ ])).toThrow(/ambiguous|duplicate/i);
112
+ });
113
+ });