noteconnection 1.8.0 → 1.9.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 (195) 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/backend/FileLoader.js +18 -4
  8. package/dist/src/backend/FileLoader.test.js +125 -0
  9. package/dist/src/backend/GraphBuilder.identity.test.js +86 -0
  10. package/dist/src/backend/GraphBuilder.js +66 -18
  11. package/dist/src/backend/GraphBuilder.matching.test.js +127 -0
  12. package/dist/src/backend/GraphMetrics.js +0 -3
  13. package/dist/src/backend/ResourceIdentity.js +155 -0
  14. package/dist/src/backend/ResourceIdentity.test.js +113 -0
  15. package/dist/src/backend/algorithms/CycleDetection.js +0 -1
  16. package/dist/src/backend/algorithms/HybridEngine.js +0 -3
  17. package/dist/src/backend/algorithms/LayoutEngine.js +0 -1
  18. package/dist/src/backend/algorithms/StatisticalAnalyzer.js +0 -4
  19. package/dist/src/backend/algorithms/TopologicalSort.js +0 -2
  20. package/dist/src/backend/build_dag.js +0 -1
  21. package/dist/src/capacitor.bridge.serialization.contract.test.js +3 -0
  22. package/dist/src/capacitor.runtime.contract.test.js +11 -2
  23. package/dist/src/core/Graph.js +138 -42
  24. package/dist/src/core/Graph.test.js +74 -0
  25. package/dist/src/core/NoteConnection.js +2 -2
  26. package/dist/src/core/PathBridge.js +265 -2
  27. package/dist/src/core/ResourceReference.js +17 -0
  28. package/dist/src/desktop.godot.pack.test.js +142 -0
  29. package/dist/src/desktop.runtime.graph.test.js +66 -0
  30. package/dist/src/export/WorkspaceExportBundle.js +44 -0
  31. package/dist/src/export/WorkspaceExportBundle.test.js +61 -0
  32. package/dist/src/foundation.release.evidence.contract.test.js +127 -9
  33. package/dist/src/foundation.sqlite.runtime.contract.test.js +6 -0
  34. package/dist/src/frontend/README.md +257 -1803
  35. package/dist/src/frontend/agent_workspace.js +665 -7
  36. package/dist/src/frontend/index.html +31 -2
  37. package/dist/src/frontend/knowledge_projection_contract.js +230 -0
  38. package/dist/src/frontend/knowledge_projection_store.js +214 -0
  39. package/dist/src/frontend/libs/path_core.js +158 -42
  40. package/dist/src/frontend/locales/en.json +32 -0
  41. package/dist/src/frontend/locales/zh.json +32 -0
  42. package/dist/src/frontend/mobile_budget_runtime.js +33 -0
  43. package/dist/src/frontend/mobile_exact_analyzer.js +393 -0
  44. package/dist/src/frontend/mobile_identity_contract.js +221 -0
  45. package/dist/src/frontend/mobile_semantic_comparator.js +264 -0
  46. package/dist/src/frontend/path.html +6 -2
  47. package/dist/src/frontend/simulationWorker.js +1 -1
  48. package/dist/src/frontend/source_manager.js +110 -4
  49. package/dist/src/frontend/storage_provider.js +661 -71
  50. package/dist/src/frontend/styles.css +107 -13
  51. package/dist/src/frontend/workspace_panes.js +111 -7
  52. package/dist/src/frontend.runtime.assets.contract.test.js +81 -0
  53. package/dist/src/graph.answer.operator.contract.test.js +72 -0
  54. package/dist/src/index.js +6 -2
  55. package/dist/src/index.knowledge-root.test.js +118 -0
  56. package/dist/src/indexing/IndexLifecycle.js +22 -0
  57. package/dist/src/indexing/IndexLifecycle.test.js +24 -0
  58. package/dist/src/knowledge.api.contract.test.js +4 -2
  59. package/dist/src/knowledge_projection.contract.test.js +134 -0
  60. package/dist/src/knowledge_projection_store.test.js +200 -0
  61. package/dist/src/learning/AnswerQualityEvaluation.js +153 -0
  62. package/dist/src/learning/AnswerQualityEvaluation.test.js +118 -0
  63. package/dist/src/learning/KnowledgeLearningPlatform.js +4687 -3892
  64. package/dist/src/learning/KnowledgeLearningPlatform.persistence.test.js +418 -0
  65. package/dist/src/learning/KnowledgeLearningPlatform.test.js +243 -6
  66. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.js +25 -1
  67. package/dist/src/learning/KnowledgeWorkspaceConversationRegression.test.js +13 -5
  68. package/dist/src/learning/MeasurementEvidenceConflicts.test.js +78 -0
  69. package/dist/src/learning/PublicAnswerContract.test.js +85 -0
  70. package/dist/src/learning/PublicEvidenceSelection.test.js +94 -0
  71. package/dist/src/learning/PublicMeasurementDisclosure.test.js +54 -0
  72. package/dist/src/learning/agentConversationExecution.js +211 -0
  73. package/dist/src/learning/agentConversationExecution.test.js +209 -0
  74. package/dist/src/learning/agentConversationSerialization.js +266 -0
  75. package/dist/src/learning/agentConversationSerialization.test.js +165 -0
  76. package/dist/src/learning/agentResponseBudget.js +179 -0
  77. package/dist/src/learning/agentResponseBudget.test.js +97 -0
  78. package/dist/src/learning/agentWorkspaceCapabilityContracts.js +65 -0
  79. package/dist/src/learning/answerReleaseReview.js +1191 -250
  80. package/dist/src/learning/answerReleaseReview.test.js +1293 -7
  81. package/dist/src/learning/answerTaskPlan.js +473 -0
  82. package/dist/src/learning/conversationComposer.js +627 -386
  83. package/dist/src/learning/conversationComposer.test.js +1403 -0
  84. package/dist/src/learning/evidenceContextAssembler.js +312 -120
  85. package/dist/src/learning/graphAnswerCoverage.js +37 -0
  86. package/dist/src/learning/graphAnswerCoverage.test.js +93 -0
  87. package/dist/src/learning/graphAnswerCoverageCalibration.js +59 -0
  88. package/dist/src/learning/graphAnswerCoverageCalibration.test.js +46 -0
  89. package/dist/src/learning/graphAnswerFacts.js +94 -0
  90. package/dist/src/learning/graphAnswerFacts.test.js +39 -0
  91. package/dist/src/learning/graphAnswerPlan.js +908 -0
  92. package/dist/src/learning/graphAnswerPlan.test.js +907 -0
  93. package/dist/src/learning/graphAnswerQualityCalibration.js +255 -0
  94. package/dist/src/learning/graphAnswerQualityCalibration.test.js +72 -0
  95. package/dist/src/learning/graphAnswerQualityPolicy.js +30 -0
  96. package/dist/src/learning/graphClaimMatcher.js +104 -0
  97. package/dist/src/learning/graphConditionedContext.js +109 -0
  98. package/dist/src/learning/graphConditionedContext.test.js +99 -0
  99. package/dist/src/learning/graphContextAssembler.js +3 -13
  100. package/dist/src/learning/graphContextAssembler.test.js +2 -2
  101. package/dist/src/learning/graphExpansionPolicy.js +22 -0
  102. package/dist/src/learning/graphExpansionPolicy.test.js +33 -0
  103. package/dist/src/learning/identityMetadata.test.js +26 -0
  104. package/dist/src/learning/index.js +1 -0
  105. package/dist/src/learning/mobileAnswerProjection.js +104 -0
  106. package/dist/src/learning/mobileAnswerProjection.test.js +102 -0
  107. package/dist/src/learning/queryBackend.js +34 -10
  108. package/dist/src/learning/queryBackend.test.js +21 -0
  109. package/dist/src/learning/ragContextPack.js +115 -16
  110. package/dist/src/learning/ragContextPack.test.js +151 -0
  111. package/dist/src/learning/ragEvidenceQuality.js +96 -0
  112. package/dist/src/learning/ragEvidenceQuality.test.js +29 -0
  113. package/dist/src/learning/ragPublicText.js +67 -4
  114. package/dist/src/learning/ragPublicText.test.js +57 -0
  115. package/dist/src/learning/ragSufficiencyJudge.js +11 -1
  116. package/dist/src/learning/ragSufficiencyProviderJudge.js +16 -26
  117. package/dist/src/learning/ragSufficiencyProviderJudge.test.js +51 -0
  118. package/dist/src/learning/requestNormalization.js +106 -0
  119. package/dist/src/learning/requestNormalization.test.js +101 -0
  120. package/dist/src/learning/store.js +116 -12
  121. package/dist/src/learning/store.test.js +299 -0
  122. package/dist/src/learning/vectorAccelerationAdapter.js +22 -6
  123. package/dist/src/learning/vectorAccelerationAdapter.test.js +30 -0
  124. package/dist/src/learning/workspaceHydration.js +168 -0
  125. package/dist/src/learning/workspaceHydration.test.js +30 -0
  126. package/dist/src/license.policy.contract.test.js +9 -4
  127. package/dist/src/markdown/MarkdownGateway.js +4 -0
  128. package/dist/src/markdown/MarkdownGateway.test.js +59 -0
  129. package/dist/src/middleware/SseResponseWriter.js +122 -0
  130. package/dist/src/middleware/SseResponseWriter.test.js +54 -0
  131. package/dist/src/middleware/auth.js +28 -7
  132. package/dist/src/middleware/auth.test.js +33 -0
  133. package/dist/src/middleware/index.js +2 -1
  134. package/dist/src/mobile.android.evidence.contract.test.js +146 -0
  135. package/dist/src/mobile.android.signing.contract.test.js +66 -0
  136. package/dist/src/mobile.artifact.contract.test.js +144 -0
  137. package/dist/src/mobile.budget.contract.test.js +90 -0
  138. package/dist/src/mobile.canonical-id.readiness.test.js +54 -0
  139. package/dist/src/mobile.graph.parity.contract.test.js +207 -0
  140. package/dist/src/mobile.identity.contract.test.js +66 -0
  141. package/dist/src/mobile.identity.corpus.manifest.test.js +72 -0
  142. package/dist/src/mobile.identity.corpus.test.js +40 -0
  143. package/dist/src/mobile.native.recovery.contract.test.js +64 -0
  144. package/dist/src/mobile.pipeline.test.js +11 -1
  145. package/dist/src/mobile.profile.contract.test.js +225 -0
  146. package/dist/src/mobile.semantic.parity.test.js +92 -0
  147. package/dist/src/mobile_exact_analyzer.test.js +113 -0
  148. package/dist/src/notemd/ContentGenerator.js +4 -9
  149. package/dist/src/notemd/FileProcessor.js +4 -9
  150. package/dist/src/notemd/NotemdService.js +12 -17
  151. package/dist/src/notemd/Translator.js +4 -9
  152. package/dist/src/notemd/progressReporter.js +9 -0
  153. package/dist/src/notemd.server.integration.test.js +119 -4
  154. package/dist/src/notemd.workflow.pipeline.test.js +1 -1
  155. package/dist/src/pathbridge.handshake.contract.test.js +29 -0
  156. package/dist/src/pathbridge.host-adapter.contract.test.js +133 -0
  157. package/dist/src/pathcore.bundle.contract.test.js +8 -0
  158. package/dist/src/platform/ExportProfile.js +28 -0
  159. package/dist/src/platform/MobileBudget.js +11 -0
  160. package/dist/src/platform/PlatformCapabilities.js +14 -0
  161. package/dist/src/platform/PlatformCapabilities.test.js +6 -0
  162. package/dist/src/query_backend.external_http.integration.test.js +2 -2
  163. package/dist/src/release.godot.mirror.contract.test.js +28 -4
  164. package/dist/src/resources/ResourceRegistry.js +40 -0
  165. package/dist/src/resources/ResourceRegistry.test.js +36 -0
  166. package/dist/src/routes/index.js +0 -4
  167. package/dist/src/routes/knowledge.js +46 -33
  168. package/dist/src/routes/knowledgePayload.js +185 -0
  169. package/dist/src/routes/knowledgePayload.test.js +53 -0
  170. package/dist/src/routes/markdown.js +8 -0
  171. package/dist/src/routes/notemd.js +990 -234
  172. package/dist/src/routes/registry.contract.test.js +5 -2
  173. package/dist/src/routes/registry.shadow.contract.test.js +22 -0
  174. package/dist/src/routes/render.js +14 -51
  175. package/dist/src/runtime.capabilities.test.js +18 -2
  176. package/dist/src/server.js +566 -1090
  177. package/dist/src/server.migration.test.js +417 -15
  178. package/dist/src/sidecar.build.lock.contract.test.js +201 -0
  179. package/dist/src/sidecar.freshness.contract.test.js +184 -0
  180. package/dist/src/sidecar.replacement.boundary.contract.test.js +6 -3
  181. package/dist/src/sidecar.supply.readiness.contract.test.js +11 -0
  182. package/dist/src/simulation.worker.offline.test.js +104 -0
  183. package/dist/src/storage.provider.capacitor.content.contract.test.js +4 -1
  184. package/dist/src/storage.provider.contract.test.js +152 -1
  185. package/dist/src/tauri.frontend.build.contract.test.js +8 -0
  186. package/dist/src/tauri.window.evidence.execution.test.js +68 -0
  187. package/dist/src/utils/RuntimePaths.js +6 -0
  188. package/dist/src/utils/RuntimePaths.test.js +27 -0
  189. package/dist/src/windows.installer.build.test.js +109 -0
  190. package/dist/src/windows.installer.payload.test.js +76 -0
  191. package/dist/src/windows.installer.runtime.test.js +69 -0
  192. package/dist/src/workspace/WorkspaceRegistry.js +13 -0
  193. package/package.json +32 -10
  194. package/dist/src/routes/data.js +0 -267
  195. package/dist/src/routes/diagnostics.js +0 -51
@@ -1,7 +1,11 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.reviewAnswerRelease = reviewAnswerRelease;
4
+ const graphAnswerCoverage_1 = require("./graphAnswerCoverage");
5
+ const graphAnswerFacts_1 = require("./graphAnswerFacts");
4
6
  const ragPublicText_1 = require("./ragPublicText");
7
+ const ragEvidenceQuality_1 = require("./ragEvidenceQuality");
8
+ const answerTaskPlan_1 = require("./answerTaskPlan");
5
9
  const INTERNAL_DIAGNOSTIC_FRAGMENTS = [
6
10
  'No scoped knowledge points matched',
7
11
  'retrieval_candidates_below_threshold',
@@ -41,8 +45,6 @@ const STRUCTURED_UNIT_ALIASES = {
41
45
  年: 'year',
42
46
  };
43
47
  const STRUCTURED_FACT_PATTERN = /(-?\d{1,4}(?:,\d{3})*(?:\.\d+)?)(?:\s*(kg\/m(?:³|3)|gpa|mpa|kpa|pa|km\/h|m\/s|km|cm|mm|ml|mb|gb|tb|kw|mw|%|percent|percentage|years?|yrs?|yr|year|°c|℃|℉|年))?/giu;
44
- const ANSWER_RELEASE_PUBLIC_CHAR_LIMIT = 900;
45
- const ANSWER_RELEASE_SENTENCE_BUDGET = 6;
46
48
  const DEFINITION_EVIDENCE_HIGHLIGHT_LIMIT = 2;
47
49
  const RAG_CLAIM_CITATION_SUPPORT_MIN_FEATURES = 2;
48
50
  const RAG_CLAIM_CITATION_SUPPORT_MIN_COVERAGE = 0.78;
@@ -95,6 +97,31 @@ const STRUCTURED_ANCHOR_STOPWORDS = new Set([
95
97
  '和',
96
98
  '的',
97
99
  ]);
100
+ const QUERY_SUBJECT_STOPWORDS = new Set([
101
+ 'a',
102
+ 'an',
103
+ 'the',
104
+ 'and',
105
+ 'are',
106
+ 'be',
107
+ 'definition',
108
+ 'define',
109
+ 'does',
110
+ 'how',
111
+ 'is',
112
+ 'meaning',
113
+ 'of',
114
+ 'what',
115
+ 'which',
116
+ 'who',
117
+ 'with',
118
+ '我',
119
+ '应该',
120
+ '通过',
121
+ '哪些',
122
+ '知识点',
123
+ '学习',
124
+ ]);
98
125
  const POLARITY_SENTENCE_SPLIT_PATTERN = /[.!?\u3002\uFF01\uFF1F;\n\r]+/u;
99
126
  const ENGLISH_POLARITY_NEGATION_PATTERN = /\b(?:not|never|no|none|cannot|is not|are not|was not|were not|do not|does not|did not|can not|could not|should not|would not|will not)\b/i;
100
127
  const CHINESE_POLARITY_NEGATION_PATTERN = /不是|并非|并不|没有|不能|无法/u;
@@ -108,12 +135,38 @@ const STATE_FRAME_SKIP_VALUE_PATTERN = /\b(?:prerequisite|before|after|depends?\
108
135
  function normalizeWhitespace(value) {
109
136
  return String(value || '').replace(/\s+/g, ' ').trim();
110
137
  }
138
+ function normalizePublicAnswerSurface(value) {
139
+ return (0, answerTaskPlan_1.formatDisplayMathBlocks)(String(value || '')
140
+ .replace(/\r\n?/gu, '\n')
141
+ .replace(/[ \t]+\n/gu, '\n')
142
+ .replace(/\n{3,}/gu, '\n\n')
143
+ .trim());
144
+ }
145
+ function isCompoundLearningDefinitionQuery(message) {
146
+ const normalized = normalizeWhitespace(String(message || '')).toLowerCase();
147
+ return isDefinitionIntentQuery(normalized)
148
+ && (/\b(?:learn|learning|study|knowledge\s+points?)\b/u.test(normalized)
149
+ || /学习|知识点|学哪些|通过哪些/u.test(normalized));
150
+ }
111
151
  function escapeRegExp(value) {
112
152
  return String(value || '').replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
113
153
  }
114
154
  function containsCjk(value) {
115
155
  return /[\u3400-\u9fff]/u.test(String(value || ''));
116
156
  }
157
+ function useChineseAnswerLanguage(context, additionalSignals = []) {
158
+ if (context.answerLanguage === 'zh') {
159
+ return true;
160
+ }
161
+ if (context.answerLanguage === 'en') {
162
+ return false;
163
+ }
164
+ return containsCjk([
165
+ context.message,
166
+ context.draftAnswer,
167
+ ...additionalSignals,
168
+ ].filter(Boolean).join(' '));
169
+ }
117
170
  function resolveScopeLabel(scope) {
118
171
  if (scope.workspaceId) {
119
172
  return String(scope.workspaceId).trim();
@@ -129,11 +182,11 @@ function resolveScopeLabel(scope) {
129
182
  }
130
183
  return '';
131
184
  }
132
- function buildFriendlyScopeFailureHint(scope) {
185
+ function buildFriendlyScopeFailureHint(scope, useChinese) {
133
186
  const scopeLabel = resolveScopeLabel(scope);
134
187
  const readinessStatus = String(scope.readiness?.status || '').trim();
135
188
  const missReason = String(scope.missDiagnostics?.reason || '').trim();
136
- if (containsCjk(scopeLabel) || containsCjk(scope.missDiagnostics?.query || '')) {
189
+ if (useChinese) {
137
190
  if (readinessStatus === 'empty_store') {
138
191
  return scopeLabel
139
192
  ? `当前范围“${scopeLabel}”还没有可检索知识。`
@@ -192,10 +245,11 @@ function buildFriendlyScopeFailureHint(scope) {
192
245
  ? `I could not ground the answer inside the current scope "${scopeLabel}".`
193
246
  : 'I could not ground the answer inside the current scope.';
194
247
  }
195
- function buildAbstentionAnswer(message, scope) {
196
- const normalizedMessage = normalizeWhitespace(message);
197
- const hint = buildFriendlyScopeFailureHint(scope);
198
- if (containsCjk(normalizedMessage)) {
248
+ function buildAbstentionAnswer(context) {
249
+ const normalizedMessage = normalizeWhitespace(context.message);
250
+ const useChinese = useChineseAnswerLanguage(context);
251
+ const hint = buildFriendlyScopeFailureHint(context.usedScope, useChinese);
252
+ if (useChinese) {
199
253
  return normalizeWhitespace(normalizedMessage
200
254
  ? `${hint} 我暂时不能对“${normalizedMessage}”给出有依据的回答。请换个说法、放宽范围,或补充相关笔记。`
201
255
  : `${hint} 请换个说法、放宽范围,或补充相关笔记。`);
@@ -205,6 +259,20 @@ function buildAbstentionAnswer(message, scope) {
205
259
  : `${hint} Refine the wording, widen the scope, or add the missing note.`);
206
260
  }
207
261
  function buildGroundedRevisionAnswer(context) {
262
+ const conflictPlanClaims = (context.graphAnswerPlan?.claims || [])
263
+ .filter((claim) => claim.required)
264
+ .map((claim) => String(claim.statement || '').trim())
265
+ .filter(Boolean);
266
+ const hasConflictEvidence = context.ragSufficiencyReview?.degradationState === 'conflict'
267
+ || (context.ragSufficiencyReview?.reasons || []).some((reason) => String(reason || '').includes('conflict_evidence_present'));
268
+ if (hasConflictEvidence && conflictPlanClaims.length > 1) {
269
+ const useChinese = useChineseAnswerLanguage(context, conflictPlanClaims);
270
+ const sentences = [];
271
+ conflictPlanClaims.forEach((claim) => appendRevisionAnswerSentence(sentences, claim, useChinese));
272
+ if (sentences.length > 0) {
273
+ return sentences.join(useChinese ? '' : ' ');
274
+ }
275
+ }
208
276
  const ragGroundedAnswer = buildRagGroundedRevisionAnswer(context);
209
277
  if (ragGroundedAnswer) {
210
278
  return ragGroundedAnswer;
@@ -223,12 +291,11 @@ function buildGroundedRevisionAnswer(context) {
223
291
  const baseAnswer = summary && title && !summaryAlreadyCarriesTitle
224
292
  ? `${title}: ${summary}`
225
293
  : (summary || title || normalizeWhitespace(context.draftAnswer));
226
- const useChinese = containsCjk([
227
- context.message,
294
+ const useChinese = useChineseAnswerLanguage(context, [
228
295
  title,
229
296
  summary,
230
297
  context.graphContext?.anchorTitle || '',
231
- ].join(' '));
298
+ ]);
232
299
  return expandAnswerWithGraphContext(baseAnswer, context, useChinese);
233
300
  }
234
301
  function stripTerminalSentencePunctuation(value) {
@@ -322,41 +389,12 @@ function removeLeadingEvidenceTitle(value, title) {
322
389
  const escapedTitle = escapeRegExp(normalizedTitle);
323
390
  return normalizeWhitespace(normalized.replace(new RegExp(`^#{0,6}\\s*${escapedTitle}\\s*[::-]?\\s*`, 'iu'), ''));
324
391
  }
325
- function isPublicEvidenceClauseBoundary(value, index) {
326
- const char = value[index];
327
- if (char === '\n' || char === '\r' || char === ';' || char === ';') {
328
- return true;
329
- }
330
- if (char === '.' && /\d/u.test(value[index - 1] || '') && /\d/u.test(value[index + 1] || '')) {
331
- return false;
332
- }
333
- return /[.!?。!?]/u.test(char);
334
- }
335
- function splitPublicEvidenceClauses(value) {
336
- const source = String(value || '');
337
- if (!source) {
338
- return [];
339
- }
340
- const clauses = [];
341
- let start = 0;
342
- for (let index = 0; index < source.length; index += 1) {
343
- if (!isPublicEvidenceClauseBoundary(source, index)) {
344
- continue;
345
- }
346
- clauses.push(source.slice(start, index));
347
- start = index + 1;
348
- }
349
- if (start < source.length) {
350
- clauses.push(source.slice(start));
351
- }
352
- return clauses;
353
- }
354
392
  function selectPublicEvidenceClause(snippet, title) {
355
393
  const cleaned = removeLeadingEvidenceTitle(stripMarkdownScaffolding(snippet), title);
356
394
  if (!cleaned) {
357
395
  return '';
358
396
  }
359
- const clauses = splitPublicEvidenceClauses(cleaned)
397
+ const clauses = (0, ragEvidenceQuality_1.segmentRagEvidenceClauses)(cleaned)
360
398
  .map((clause) => normalizeWhitespace(clause))
361
399
  .map((clause) => (0, ragPublicText_1.naturalizeRagPublicEvidenceClause)(clause))
362
400
  .filter((clause) => (clause.length >= 8
@@ -365,9 +403,66 @@ function selectPublicEvidenceClause(snippet, title) {
365
403
  && !isPromptArtifactClause(clause)
366
404
  && !/^[::\-–—]+$/u.test(clause)));
367
405
  const selected = clauses[0] || cleaned;
368
- return selected.length > 120
369
- ? `${selected.slice(0, 118).trim()}...`
370
- : selected;
406
+ if (!hasBalancedPublicMathDelimiters(selected)) {
407
+ return '';
408
+ }
409
+ return selectCompletePublicEvidenceBoundary(selected);
410
+ }
411
+ const PUBLIC_EVIDENCE_MAX_CHARS = 480;
412
+ const PUBLIC_EVIDENCE_HARD_MAX_CHARS = 720;
413
+ function hasBalancedPublicMathDelimiters(value) {
414
+ const source = String(value || '');
415
+ const displayMathCount = (source.match(/(?<!\\)\$\$/gu) || []).length;
416
+ if (displayMathCount % 2 !== 0) {
417
+ return false;
418
+ }
419
+ const inlineSource = source.replace(/(?<!\\)\$\$/gu, '');
420
+ const inlineMathCount = (inlineSource.match(/(?<!\\)\$/gu) || []).length;
421
+ return inlineMathCount % 2 === 0;
422
+ }
423
+ function selectCompletePublicEvidenceBoundary(value) {
424
+ const normalized = normalizeWhitespace(value);
425
+ if (normalized.length <= PUBLIC_EVIDENCE_MAX_CHARS) {
426
+ return normalized;
427
+ }
428
+ let inlineMathOpen = false;
429
+ let displayMathOpen = false;
430
+ const boundaries = [];
431
+ for (let index = 0; index < normalized.length; index += 1) {
432
+ const character = normalized[index];
433
+ if (character === '\\') {
434
+ index += 1;
435
+ continue;
436
+ }
437
+ if (character === '$') {
438
+ if (normalized[index + 1] === '$') {
439
+ displayMathOpen = !displayMathOpen;
440
+ index += 1;
441
+ }
442
+ else if (!displayMathOpen) {
443
+ inlineMathOpen = !inlineMathOpen;
444
+ }
445
+ continue;
446
+ }
447
+ if (!inlineMathOpen
448
+ && !displayMathOpen
449
+ && /[.!?\u3002\uFF01\uFF1F]/u.test(character)
450
+ && !(character === '.'
451
+ && /\d/u.test(normalized[index - 1] || '')
452
+ && /\d/u.test(normalized[index + 1] || ''))
453
+ && (index + 1 >= normalized.length || /\s/u.test(normalized[index + 1]))) {
454
+ boundaries.push(index + 1);
455
+ }
456
+ }
457
+ const boundedBoundary = boundaries
458
+ .filter((boundary) => boundary <= PUBLIC_EVIDENCE_MAX_CHARS)
459
+ .pop()
460
+ || boundaries.find((boundary) => boundary <= PUBLIC_EVIDENCE_HARD_MAX_CHARS);
461
+ if (!boundedBoundary) {
462
+ return '';
463
+ }
464
+ const complete = normalizeWhitespace(normalized.slice(0, boundedBoundary));
465
+ return hasBalancedPublicMathDelimiters(complete) ? complete : '';
371
466
  }
372
467
  function collectDefinitionEvidenceHighlights(context) {
373
468
  const leadingPoint = context.knowledgePoints[0];
@@ -433,39 +528,6 @@ function buildRevisionGraphConnectionSentence(context, useChinese) {
433
528
  }
434
529
  return `The strongest graph path runs through ${pathTitles.join(' -> ')}`;
435
530
  }
436
- function normalizeRevisionGraphComparableTitle(value) {
437
- return normalizeDefinitionEvidenceTitle(value).toLowerCase();
438
- }
439
- function collectRevisionGraphWindowTitles(graphContext, windowKey, limit) {
440
- if (!graphContext || !Array.isArray(graphContext[windowKey])) {
441
- return [];
442
- }
443
- const anchorProfile = graphContext.anchorGraphProfile && typeof graphContext.anchorGraphProfile === 'object'
444
- ? graphContext.anchorGraphProfile
445
- : null;
446
- const anchorAtomId = normalizeWhitespace(String(graphContext.anchorAtomId || anchorProfile?.atomId || '').trim());
447
- const anchorTitle = normalizeRevisionGraphComparableTitle(graphContext.anchorTitle || anchorProfile?.title || '');
448
- const seen = new Set();
449
- const titles = [];
450
- for (const node of graphContext[windowKey] || []) {
451
- const atomId = normalizeWhitespace(String(node && node.atomId || '').trim());
452
- const title = normalizeDefinitionEvidenceTitle(node && node.title);
453
- const comparableTitle = normalizeRevisionGraphComparableTitle(title);
454
- if (!title || (atomId && atomId === anchorAtomId) || (comparableTitle && comparableTitle === anchorTitle)) {
455
- continue;
456
- }
457
- const key = comparableTitle || atomId;
458
- if (seen.has(key)) {
459
- continue;
460
- }
461
- seen.add(key);
462
- titles.push(title);
463
- if (titles.length >= limit) {
464
- break;
465
- }
466
- }
467
- return titles;
468
- }
469
531
  function buildRevisionGraphProfileSentence(context, useChinese) {
470
532
  const graphContext = context.graphContext;
471
533
  if (!graphContext) {
@@ -474,48 +536,15 @@ function buildRevisionGraphProfileSentence(context, useChinese) {
474
536
  const anchorProfile = graphContext.anchorGraphProfile && typeof graphContext.anchorGraphProfile === 'object'
475
537
  ? graphContext.anchorGraphProfile
476
538
  : null;
477
- const anchorTitle = normalizeDefinitionEvidenceTitle(graphContext.anchorTitle || anchorProfile?.title || '');
478
- const predecessorTitles = collectRevisionGraphWindowTitles(graphContext, 'predecessorWindow', 2);
479
- const successorTitles = collectRevisionGraphWindowTitles(graphContext, 'successorWindow', 2);
480
- const degreeParts = [];
481
- const inDegree = anchorProfile ? Number(anchorProfile.inDegree) : NaN;
482
- const outDegree = anchorProfile ? Number(anchorProfile.outDegree) : NaN;
483
- if (Number.isFinite(inDegree)) {
484
- degreeParts.push(useChinese ? `入度为 ${inDegree}` : `${inDegree} incoming`);
485
- }
486
- if (Number.isFinite(outDegree)) {
487
- degreeParts.push(useChinese ? `出度为 ${outDegree}` : `${outDegree} outgoing`);
488
- }
489
- if (degreeParts.length <= 0 && predecessorTitles.length <= 0 && successorTitles.length <= 0) {
539
+ const facts = (0, graphAnswerFacts_1.collectGraphAnswerFacts)(graphContext, {
540
+ anchorAtomId: graphContext.anchorAtomId || anchorProfile?.atomId || '',
541
+ anchorTitle: graphContext.anchorTitle || anchorProfile?.title || '',
542
+ normalizeTitle: normalizeDefinitionEvidenceTitle,
543
+ });
544
+ if (!facts) {
490
545
  return '';
491
546
  }
492
- if (useChinese) {
493
- const fragments = [];
494
- if (degreeParts.length > 0) {
495
- fragments.push(`${anchorTitle || '当前锚点'}在当前图中的${degreeParts.join(',')}`);
496
- }
497
- if (predecessorTitles.length > 0) {
498
- fragments.push(`紧邻前置节点包括 ${predecessorTitles.join('、')}`);
499
- }
500
- if (successorTitles.length > 0) {
501
- fragments.push(`后续分支包括 ${successorTitles.join('、')}`);
502
- }
503
- return fragments.join(',');
504
- }
505
- const fragments = [];
506
- if (degreeParts.length > 0) {
507
- fragments.push(`${anchorTitle || 'The current anchor'} has ${degreeParts.join(' and ')} links in the current graph`);
508
- }
509
- if (predecessorTitles.length > 0 && successorTitles.length > 0) {
510
- fragments.push(`its immediate predecessors include ${predecessorTitles.join(', ')}, and likely next nodes include ${successorTitles.join(', ')}`);
511
- }
512
- else if (predecessorTitles.length > 0) {
513
- fragments.push(`its immediate predecessors include ${predecessorTitles.join(', ')}`);
514
- }
515
- else if (successorTitles.length > 0) {
516
- fragments.push(`its likely next nodes include ${successorTitles.join(', ')}`);
517
- }
518
- return fragments.join('; ');
547
+ return (0, graphAnswerFacts_1.formatGraphAnswerProfileSentence)(facts, useChinese);
519
548
  }
520
549
  function expandAnswerWithGraphContext(baseAnswer, context, useChinese, extraSentences = []) {
521
550
  const sentences = [];
@@ -523,13 +552,13 @@ function expandAnswerWithGraphContext(baseAnswer, context, useChinese, extraSent
523
552
  extraSentences.forEach((sentence) => appendRevisionAnswerSentence(sentences, sentence, useChinese));
524
553
  appendRevisionAnswerSentence(sentences, buildRevisionGraphConnectionSentence(context, useChinese), useChinese);
525
554
  appendRevisionAnswerSentence(sentences, buildRevisionGraphProfileSentence(context, useChinese), useChinese);
526
- return sentences.slice(0, ANSWER_RELEASE_SENTENCE_BUDGET).join(useChinese ? '' : ' ');
555
+ return sentences.join(useChinese ? '' : ' ');
527
556
  }
528
557
  function expandRagGroundedAnswer(baseAnswer, useChinese, evidenceSentences = []) {
529
558
  const sentences = [];
530
559
  appendRevisionAnswerSentence(sentences, baseAnswer, useChinese);
531
560
  evidenceSentences.forEach((sentence) => appendRevisionAnswerSentence(sentences, sentence, useChinese));
532
- return sentences.slice(0, ANSWER_RELEASE_SENTENCE_BUDGET).join(useChinese ? '' : ' ');
561
+ return sentences.join(useChinese ? '' : ' ');
533
562
  }
534
563
  function hasUsableRagEvidenceContext(context) {
535
564
  const pack = context.ragContextPack;
@@ -651,7 +680,7 @@ function splitRagPublicEvidenceClauses(fragment, options = {}) {
651
680
  if (!cleaned) {
652
681
  return [];
653
682
  }
654
- const clauses = splitPublicEvidenceClauses(cleaned)
683
+ const clauses = (0, ragEvidenceQuality_1.segmentRagEvidenceClauses)(cleaned)
655
684
  .map((clause) => normalizeWhitespace(clause))
656
685
  .map((clause) => (0, ragPublicText_1.naturalizeRagPublicEvidenceClause)(clause))
657
686
  .filter((clause) => (clause.length >= 8
@@ -678,6 +707,7 @@ function collectRagPublicEvidenceClauses(context, roles, limit, excludedClauses
678
707
  clause,
679
708
  queryTerms: collectRagPublicClauseQueryTerms(clause, queryTerms),
680
709
  headingQueryTerms,
710
+ qualityScore: (0, ragEvidenceQuality_1.scoreRagEvidenceClause)(clause).score,
681
711
  order,
682
712
  });
683
713
  order += 1;
@@ -698,10 +728,13 @@ function collectRagPublicEvidenceClauses(context, roles, limit, excludedClauses
698
728
  if (rightScore !== leftScore) {
699
729
  return rightScore - leftScore;
700
730
  }
731
+ if (right.qualityScore !== left.qualityScore) {
732
+ return right.qualityScore - left.qualityScore;
733
+ }
701
734
  return left.order - right.order;
702
735
  });
703
736
  rankedCandidates.forEach((candidate) => {
704
- if (clauses.length >= limit || ragClauseAlreadyCovered(candidate.clause, selected)) {
737
+ if ((limit !== undefined && clauses.length >= limit) || ragClauseAlreadyCovered(candidate.clause, selected)) {
705
738
  return;
706
739
  }
707
740
  selected.push(candidate.clause);
@@ -709,33 +742,6 @@ function collectRagPublicEvidenceClauses(context, roles, limit, excludedClauses
709
742
  });
710
743
  return clauses;
711
744
  }
712
- function ragRevisionDirectClauseLimit(profile) {
713
- if (profile === 'compare') {
714
- return 1;
715
- }
716
- if (profile === 'how_to') {
717
- return 3;
718
- }
719
- if (profile === 'causal') {
720
- return 2;
721
- }
722
- return 1;
723
- }
724
- function ragRevisionDocumentClauseLimit(profile) {
725
- if (profile === 'compare') {
726
- return 2;
727
- }
728
- if (profile === 'how_to' || profile === 'causal') {
729
- return 2;
730
- }
731
- return 2;
732
- }
733
- function ragRevisionGraphClauseLimit(profile) {
734
- if (profile === 'compare' || profile === 'how_to' || profile === 'causal') {
735
- return 2;
736
- }
737
- return 1;
738
- }
739
745
  function ragRevisionDocumentClauseOptions(profile, queryTerms) {
740
746
  if (profile === 'compare') {
741
747
  return {
@@ -775,36 +781,39 @@ function ragRevisionGraphClauseOptions(profile, queryTerms) {
775
781
  minimumLeafHeadingTermCount: queryTerms.length >= 3 ? queryTerms.length : 0,
776
782
  };
777
783
  }
784
+ function hasPlannedGraphAnswerClaims(context) {
785
+ return (context.graphAnswerPlan?.claims || []).some((claim) => (normalizeWhitespace(String(claim.statement || '')).length > 0));
786
+ }
778
787
  function buildRagGroundedRevisionAnswer(context) {
779
- if (!hasUsableRagEvidenceContext(context)) {
788
+ if (hasPlannedGraphAnswerClaims(context) || !hasUsableRagEvidenceContext(context)) {
780
789
  return '';
781
790
  }
782
- const useChinese = containsCjk([
783
- context.message,
784
- context.draftAnswer,
791
+ const useChinese = useChineseAnswerLanguage(context, [
785
792
  context.graphContext?.anchorTitle || '',
786
793
  ...(context.ragContextPack?.fragments || []).slice(0, 4).map((fragment) => fragment.text),
787
- ].join(' '));
794
+ ]);
788
795
  const queryTerms = extractRagPublicQueryTerms(context.message);
789
796
  const profile = resolveRagPublicAnswerProfile(context.message);
790
797
  const isCompareQuery = profile === 'compare';
798
+ const rejectAnswerControlClause = ragPublicText_1.shouldRejectPublicEvidenceClause;
791
799
  const rejectCompareProcedureClause = isCompareQuery
792
- ? (clause) => (0, ragPublicText_1.shouldRejectCompareProcedureEvidenceClause)(clause, context.message)
793
- : undefined;
794
- const directClauses = collectRagPublicEvidenceClauses(context, new Set(['direct_support']), ragRevisionDirectClauseLimit(profile), [], {
800
+ ? (clause) => (rejectAnswerControlClause(clause)
801
+ || (0, ragPublicText_1.shouldRejectCompareProcedureEvidenceClause)(clause, context.message))
802
+ : rejectAnswerControlClause;
803
+ const directClauses = collectRagPublicEvidenceClauses(context, new Set(['direct_support']), undefined, [], {
795
804
  preserveLeadingHeading: isCompareQuery,
796
805
  rejectClause: rejectCompareProcedureClause,
797
806
  });
798
- const documentClauses = collectRagPublicEvidenceClauses(context, new Set(['parent_context', 'adjacent_context']), ragRevisionDocumentClauseLimit(profile), directClauses, {
807
+ const documentClauses = collectRagPublicEvidenceClauses(context, new Set(['parent_context', 'adjacent_context']), undefined, directClauses, {
799
808
  ...ragRevisionDocumentClauseOptions(profile, queryTerms),
800
809
  rejectClause: rejectCompareProcedureClause,
801
810
  });
802
811
  const hasConflictEvidence = context.ragSufficiencyReview?.degradationState === 'conflict'
803
812
  || (context.ragSufficiencyReview?.reasons || []).some((reason) => String(reason || '').includes('conflict_evidence_present'));
804
813
  const conflictClauses = hasConflictEvidence
805
- ? collectRagPublicEvidenceClauses(context, new Set(['conflict']), 3, [...directClauses, ...documentClauses])
814
+ ? collectRagPublicEvidenceClauses(context, new Set(['conflict']), undefined, [...directClauses, ...documentClauses])
806
815
  : [];
807
- const graphClauses = collectRagPublicEvidenceClauses(context, new Set(['graph_neighbor_support']), ragRevisionGraphClauseLimit(profile), [...directClauses, ...documentClauses, ...conflictClauses], {
816
+ const graphClauses = collectRagPublicEvidenceClauses(context, new Set(['graph_neighbor_support']), undefined, [...directClauses, ...documentClauses, ...conflictClauses], {
808
817
  ...ragRevisionGraphClauseOptions(profile, queryTerms),
809
818
  rejectClause: rejectCompareProcedureClause,
810
819
  });
@@ -816,11 +825,8 @@ function buildRagGroundedRevisionAnswer(context) {
816
825
  if (!baseAnswer) {
817
826
  return '';
818
827
  }
819
- const additionalDirectClauses = profile === 'compare'
820
- ? []
821
- : directClauses.slice(1);
822
828
  const extraSentences = [
823
- ...additionalDirectClauses,
829
+ ...directClauses.slice(1),
824
830
  ...conflictClauses,
825
831
  ...documentClauses,
826
832
  ...graphClauses,
@@ -873,12 +879,7 @@ function buildDefinitionIntentRevisionAnswer(context, supportFrame) {
873
879
  const subject = normalizeWhitespace(String(leadingPoint?.title || supportFrame.subject || ''));
874
880
  const value = stripTerminalSentencePunctuation(supportFrame.value);
875
881
  const normalizedSurface = normalizeWhitespace(String(supportFrame.surface || ''));
876
- const useChinese = containsCjk([
877
- context.message,
878
- subject,
879
- value,
880
- normalizedSurface,
881
- ].join(' '));
882
+ const useChinese = useChineseAnswerLanguage(context, [subject, value, normalizedSurface]);
882
883
  if (!useChinese && normalizedSurface) {
883
884
  const canonicalizedSurface = (subject
884
885
  && supportFrame.subject
@@ -912,33 +913,18 @@ function buildDefinitionIntentRevisionAnswer(context, supportFrame) {
912
913
  ]);
913
914
  }
914
915
  function buildReleasedPublicAnswer(context, draftAnswer) {
915
- if (!isDefinitionIntentQuery(context.message)) {
916
- return draftAnswer;
917
- }
918
- const ragGroundedAnswer = buildRagGroundedRevisionAnswer(context);
919
- if (ragGroundedAnswer) {
920
- return ragGroundedAnswer;
916
+ if (isDefinitionIntentQuery(context.message)) {
917
+ const ragGroundedAnswer = buildRagGroundedRevisionAnswer(context);
918
+ if (ragGroundedAnswer && !normalizeWhitespace(draftAnswer).includes(normalizeWhitespace(ragGroundedAnswer))) {
919
+ return normalizeWhitespace(`${draftAnswer} ${ragGroundedAnswer}`);
920
+ }
921
921
  }
922
- const useChinese = containsCjk([
923
- context.message,
924
- draftAnswer,
925
- context.knowledgePoints[0]?.title || '',
926
- context.graphContext?.anchorTitle || '',
927
- ].join(' '));
928
- return expandAnswerWithGraphContext(draftAnswer, context, useChinese, [
929
- buildDefinitionAugmentationSentence(context, useChinese),
930
- buildDefinitionEvidenceHighlightSentence(context, useChinese),
931
- ]);
922
+ return draftAnswer;
932
923
  }
933
924
  function buildGraphOrderRevisionAnswer(context, conflict) {
934
925
  const earlierTitle = normalizeWhitespace(conflict.evidence.earlierTitle);
935
926
  const laterTitle = normalizeWhitespace(conflict.evidence.laterTitle);
936
- const useChinese = containsCjk([
937
- context.message,
938
- context.draftAnswer,
939
- earlierTitle,
940
- laterTitle,
941
- ].join(' '));
927
+ const useChinese = useChineseAnswerLanguage(context, [earlierTitle, laterTitle]);
942
928
  if (useChinese) {
943
929
  return conflict.evidence.relationKind === 'prerequisite'
944
930
  ? `${earlierTitle}是${laterTitle}的前置条件。`
@@ -951,12 +937,7 @@ function buildGraphOrderRevisionAnswer(context, conflict) {
951
937
  function buildGraphCausalRevisionAnswer(context, conflict) {
952
938
  const causeTitle = normalizeWhitespace(conflict.evidence.causeTitle);
953
939
  const effectTitle = normalizeWhitespace(conflict.evidence.effectTitle);
954
- const useChinese = containsCjk([
955
- context.message,
956
- context.draftAnswer,
957
- causeTitle,
958
- effectTitle,
959
- ].join(' '));
940
+ const useChinese = useChineseAnswerLanguage(context, [causeTitle, effectTitle]);
960
941
  if (useChinese) {
961
942
  return `${causeTitle}导致${effectTitle}。`;
962
943
  }
@@ -965,12 +946,7 @@ function buildGraphCausalRevisionAnswer(context, conflict) {
965
946
  function buildGraphComparisonRevisionAnswer(context, conflict) {
966
947
  const leftTitle = normalizeWhitespace(conflict.evidence.leftTitle);
967
948
  const rightTitle = normalizeWhitespace(conflict.evidence.rightTitle);
968
- const useChinese = containsCjk([
969
- context.message,
970
- context.draftAnswer,
971
- leftTitle,
972
- rightTitle,
973
- ].join(' '));
949
+ const useChinese = useChineseAnswerLanguage(context, [leftTitle, rightTitle]);
974
950
  if (useChinese) {
975
951
  return conflict.evidence.relationKind === 'contrast'
976
952
  ? `${leftTitle}与${rightTitle}不同。`
@@ -995,13 +971,11 @@ function buildTemporalValidityRevisionAnswer(context, conflict) {
995
971
  || context.knowledgePoints[0]?.title
996
972
  || conflict.invalidKnowledgePointTitles[0]
997
973
  || '');
998
- const useChinese = containsCjk([
999
- context.message,
1000
- context.draftAnswer,
974
+ const useChinese = useChineseAnswerLanguage(context, [
1001
975
  anchorTitle,
1002
976
  ...conflict.invalidKnowledgePointTitles,
1003
977
  ...conflict.warningReasons,
1004
- ].join(' '));
978
+ ]);
1005
979
  if (useChinese) {
1006
980
  if (anchorTitle) {
1007
981
  return `关于${anchorTitle}的当前命中证据带有时序警告,我不能把它直接当作当前结论发布。`;
@@ -1036,12 +1010,7 @@ function buildStructuredComparisonRevisionAnswer(context, conflict) {
1036
1010
  if (!correctedLeft || !correctedRight) {
1037
1011
  return buildGroundedRevisionAnswer(context);
1038
1012
  }
1039
- const useChinese = containsCjk([
1040
- context.message,
1041
- context.draftAnswer,
1042
- correctedLeft,
1043
- correctedRight,
1044
- ].join(' '));
1013
+ const useChinese = useChineseAnswerLanguage(context, [correctedLeft, correctedRight]);
1045
1014
  if (useChinese) {
1046
1015
  return `${correctedLeft}高于${correctedRight}。`;
1047
1016
  }
@@ -1057,6 +1026,7 @@ function buildSupportCandidates(context) {
1057
1026
  candidates.push({
1058
1027
  label: title,
1059
1028
  text,
1029
+ sourceText: String(fragment.text || ''),
1060
1030
  });
1061
1031
  }
1062
1032
  });
@@ -1069,6 +1039,7 @@ function buildSupportCandidates(context) {
1069
1039
  candidates.push({
1070
1040
  label: title,
1071
1041
  text,
1042
+ sourceText: String(citation.snippet || ''),
1072
1043
  });
1073
1044
  }
1074
1045
  });
@@ -1080,11 +1051,35 @@ function buildSupportCandidates(context) {
1080
1051
  candidates.push({
1081
1052
  label: title,
1082
1053
  text,
1054
+ sourceText: String(point.evidenceSnippet || point.summary || ''),
1083
1055
  });
1084
1056
  }
1085
1057
  });
1086
1058
  return candidates;
1087
1059
  }
1060
+ function removeSupportedSourceLabels(context, answer) {
1061
+ const sourceClausesByLabel = new Map();
1062
+ const clauseKey = (text) => stripTerminalSentencePunctuation(normalizeWhitespace((0, ragPublicText_1.naturalizeRagPublicEvidenceClause)(text)));
1063
+ for (const source of buildSupportCandidates(context)) {
1064
+ const label = normalizeWhitespace(source.label).toLowerCase();
1065
+ const clauses = sourceClausesByLabel.get(label) || new Set();
1066
+ for (const clause of (0, ragEvidenceQuality_1.segmentRagEvidenceClauses)(source.sourceText))
1067
+ clauses.add(clauseKey(clause));
1068
+ sourceClausesByLabel.set(label, clauses);
1069
+ }
1070
+ return (0, ragEvidenceQuality_1.segmentRagEvidenceClauses)(answer).map(clause => {
1071
+ for (const separator of clause.matchAll(/[::]/gu)) {
1072
+ const label = clauseKey(clause.slice(0, separator.index)).toLowerCase();
1073
+ const sourceClauses = sourceClausesByLabel.get(label);
1074
+ const body = clause.slice(separator.index + 1).trim();
1075
+ // A source prefix is presentation only when the complete remaining
1076
+ // clause is quoted from that source. Labels cannot legitimize a changed assertion.
1077
+ if (body && sourceClauses?.has(clauseKey(body)))
1078
+ return body;
1079
+ }
1080
+ return clause;
1081
+ }).join('\n');
1082
+ }
1088
1083
  function isGraphOrderSupportedRelationKind(value) {
1089
1084
  return value === 'prerequisite' || value === 'sequence';
1090
1085
  }
@@ -1248,11 +1243,18 @@ function buildPolarityComparableFeatures(value) {
1248
1243
  .filter((feature) => feature.length >= 2)
1249
1244
  .filter((feature) => !STRUCTURED_ANCHOR_STOPWORDS.has(feature));
1250
1245
  }
1251
- function extractPolaritySentences(value) {
1246
+ function stripLearningRoutePresentation(value) {
1252
1247
  return String(value || '')
1248
+ .replace(/(?:^|\n)\s*#{1,6}\s*(?:建议学习路径|推荐学习路径|Suggested learning path)\s*:?[^\n]*\n[\s\S]*$/imu, ' ')
1249
+ .replace(/(?:建议学习路径|推荐学习路径|Suggested learning path)\s*[::][\s\S]*?(?=。|\.$|$)/giu, ' ')
1250
+ .trim();
1251
+ }
1252
+ function extractPolaritySentences(value, responseMode = 'slim') {
1253
+ return stripLearningRoutePresentation(value)
1253
1254
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
1254
1255
  .map((sentence) => normalizeWhitespace(sentence))
1255
1256
  .filter((sentence) => sentence.length >= 8)
1257
+ .filter((sentence) => responseMode !== 'full' || !isMarkdownLabelOnlySentence(sentence))
1256
1258
  .map((sentence) => ({
1257
1259
  surface: sentence,
1258
1260
  polarity: classifySentencePolarity(sentence),
@@ -1260,6 +1262,13 @@ function extractPolaritySentences(value) {
1260
1262
  }))
1261
1263
  .filter((sentence) => sentence.comparableFeatures.length >= 2);
1262
1264
  }
1265
+ function isMarkdownLabelOnlySentence(value) {
1266
+ const normalized = normalizeWhitespace(String(value || ''));
1267
+ if (!normalized) {
1268
+ return true;
1269
+ }
1270
+ return /^(?:#{1,6}\s*|[|]\s*|[-*+]\s+)?(?:\*\*)?[^.!?\u3002\uFF01\uFF1F]{2,}(?:\*\*)?[::]\s*$/u.test(normalized);
1271
+ }
1263
1272
  function computePolarityFeatureOverlap(answerSentence, supportSentence) {
1264
1273
  if (answerSentence.comparableFeatures.length <= 0 || supportSentence.comparableFeatures.length <= 0) {
1265
1274
  return 0;
@@ -1825,7 +1834,7 @@ function extractStateFrames(value) {
1825
1834
  connectorKind: 'definition',
1826
1835
  },
1827
1836
  ];
1828
- return String(value || '')
1837
+ return stripLearningRoutePresentation(value)
1829
1838
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
1830
1839
  .map((sentence) => normalizeWhitespace(sentence))
1831
1840
  .filter((sentence) => sentence.length >= 8 || (containsCjk(sentence) && sentence.length >= 5))
@@ -1861,6 +1870,9 @@ function buildSubjectFrame(surface, subject, tail) {
1861
1870
  tailFeatures,
1862
1871
  };
1863
1872
  }
1873
+ function collapseDisplayMathForSemanticParsing(value) {
1874
+ return String(value || '').replace(/\s*(?<!\\)\$\$([\s\S]*?)(?<!\\)\$\$\s*/gu, (_match, expression) => ` $$${normalizeWhitespace(expression)}$$ `);
1875
+ }
1864
1876
  function buildAttributeFrame(surface, subject, value) {
1865
1877
  const normalizedSurface = normalizeWhitespace(surface);
1866
1878
  const normalizedSubject = normalizeAttributeFrameSubject(subject);
@@ -1889,7 +1901,7 @@ function extractSubjectFrames(value) {
1889
1901
  /^(.{1,80}?)\s+(?:is|are|was|were|remains|stays|equals|means|refers to|belongs to|has|contains)\s+(.+)$/iu,
1890
1902
  /^(.{1,24}?)(?:是|为|等于|指|属于|有|包含)(.+)$/u,
1891
1903
  ];
1892
- return String(value || '')
1904
+ return collapseDisplayMathForSemanticParsing(stripLearningRoutePresentation(value))
1893
1905
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
1894
1906
  .map((sentence) => normalizeWhitespace(sentence))
1895
1907
  .filter((sentence) => sentence.length >= 8 || (containsCjk(sentence) && sentence.length >= 5))
@@ -1900,7 +1912,11 @@ function extractSubjectFrames(value) {
1900
1912
  continue;
1901
1913
  }
1902
1914
  const frame = buildSubjectFrame(sentence, String(match[1] || ''), String(match[2] || ''));
1903
- return frame ? [frame] : [];
1915
+ return frame
1916
+ && !isStandaloneVariableDefinitionClaim(frame.surface)
1917
+ && !/(?<!\\)\$[^$\n]+?(?<!\\)\$/u.test(frame.subject)
1918
+ ? [frame]
1919
+ : [];
1904
1920
  }
1905
1921
  return [];
1906
1922
  });
@@ -1910,7 +1926,7 @@ function extractAttributeFrames(value) {
1910
1926
  /^(.{1,80}?)\s+(?:has|have|had|features?)\s+(.+)$/iu,
1911
1927
  /^(.{1,24}?)(?:有|具有|带有)(.+)$/u,
1912
1928
  ];
1913
- return String(value || '')
1929
+ return stripLearningRoutePresentation(value)
1914
1930
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
1915
1931
  .map((sentence) => normalizeWhitespace(sentence))
1916
1932
  .filter((sentence) => sentence.length >= 8 || (containsCjk(sentence) && sentence.length >= 5))
@@ -2027,7 +2043,7 @@ function extractContainmentFrames(value) {
2027
2043
  /^(.{1,80}?)\s+(?:is|are|was|were)\s+filled\s+with\s+(.+)$/iu,
2028
2044
  /^(.{1,24}?)(?:装有|盛有|含有|包含)(.+)$/u,
2029
2045
  ];
2030
- return String(value || '')
2046
+ return stripLearningRoutePresentation(value)
2031
2047
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
2032
2048
  .map((sentence) => normalizeWhitespace(sentence))
2033
2049
  .filter((sentence) => sentence.length >= 8 || (containsCjk(sentence) && sentence.length >= 5))
@@ -2054,7 +2070,7 @@ function extractPurposeFrames(value) {
2054
2070
  /^(.{1,24}?)(?:可)?用来(.+)$/u,
2055
2071
  /^(.{1,24}?)(?:被)?用作(.+)$/u,
2056
2072
  ];
2057
- return String(value || '')
2073
+ return stripLearningRoutePresentation(value)
2058
2074
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
2059
2075
  .map((sentence) => normalizeWhitespace(sentence))
2060
2076
  .filter((sentence) => sentence.length >= 8 || (containsCjk(sentence) && sentence.length >= 5))
@@ -2097,7 +2113,7 @@ function extractDependencyFrames(value) {
2097
2113
  dependencyIndex: 1,
2098
2114
  },
2099
2115
  ];
2100
- return String(value || '')
2116
+ return stripLearningRoutePresentation(value)
2101
2117
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
2102
2118
  .map((sentence) => normalizeWhitespace(sentence))
2103
2119
  .filter((sentence) => sentence.length >= 8 || (containsCjk(sentence) && sentence.length >= 5))
@@ -2149,7 +2165,7 @@ function extractLocationFrames(value) {
2149
2165
  /^(.{1,80}?)\s+lies?\s+(?:in|within)\s+(.+)$/iu,
2150
2166
  /^(.{1,24}?)(?:位于|位於|坐落于|坐落於|处于|處於)(.+)$/u,
2151
2167
  ];
2152
- return String(value || '')
2168
+ return stripLearningRoutePresentation(value)
2153
2169
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
2154
2170
  .map((sentence) => normalizeWhitespace(sentence))
2155
2171
  .filter((sentence) => sentence.length >= 8 || (containsCjk(sentence) && sentence.length >= 5))
@@ -2175,7 +2191,7 @@ function extractCompositionFrames(value) {
2175
2191
  /^(.{1,24}?)(?:是)?由(.+?)(?:组成|构成|構成)(?:的.+)?$/u,
2176
2192
  /^(.{1,24}?)(?:被定义为|定义为|被定義為|定義為).+?由(.+?)(?:组成|构成|構成)(?:的.+)?$/u,
2177
2193
  ];
2178
- return String(value || '')
2194
+ return stripLearningRoutePresentation(value)
2179
2195
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
2180
2196
  .map((sentence) => normalizeWhitespace(sentence))
2181
2197
  .filter((sentence) => sentence.length >= 8 || (containsCjk(sentence) && sentence.length >= 5))
@@ -2192,10 +2208,54 @@ function extractCompositionFrames(value) {
2192
2208
  });
2193
2209
  }
2194
2210
  function subjectFramesShareTail(answerFrame, supportFrame) {
2211
+ const answerQualifiers = collectScopeQualifierTokens(`${answerFrame.subject} ${answerFrame.tail}`);
2212
+ const supportQualifiers = collectScopeQualifierTokens(`${supportFrame.subject} ${supportFrame.tail}`);
2213
+ if (answerQualifiers.size > 0
2214
+ && supportQualifiers.size > 0
2215
+ && !Array.from(answerQualifiers).some((qualifier) => supportQualifiers.has(qualifier))) {
2216
+ return false;
2217
+ }
2195
2218
  const overlapRatio = computeFeatureOverlapRatio(answerFrame.tailFeatures, supportFrame.tailFeatures);
2196
2219
  const jaccard = computeFeatureJaccard(answerFrame.tailFeatures, supportFrame.tailFeatures);
2197
2220
  return overlapRatio >= 0.75 || jaccard >= 0.7;
2198
2221
  }
2222
+ const SUBJECT_SCOPE_QUALIFIER_FEATURES = new Set([
2223
+ 'staging', 'production', 'development', 'dev', 'test', 'testing', 'qa', 'sandbox',
2224
+ 'windows', 'android', 'ios', 'linux', 'macos', 'mac',
2225
+ 'current', 'planned', 'historical', 'history', 'legacy', 'future', 'past',
2226
+ '生产', '预发布', '开发', '测试', '版本',
2227
+ ]);
2228
+ function collectScopeQualifierTokens(value) {
2229
+ const normalized = normalizeWhitespace(value).toLowerCase();
2230
+ const qualifiers = normalized.match(/\b(?:staging|production|development|dev|test|testing|qa|sandbox|windows|android|ios|linux|macos|mac|current|planned|historical|history|legacy|future|past)\b|\b(?:version|v)\s*\d+(?:\.\d+)*\b|(?:生产|预发布|开发|测试|当前|计划|历史|旧版|版本\s*\d+(?:\.\d+)*)/gu) || [];
2231
+ return new Set(qualifiers.map((qualifier) => {
2232
+ const normalizedQualifier = normalizeWhitespace(qualifier);
2233
+ const versionMatch = normalizedQualifier.match(/^(?:version|v)\s*(\d+(?:\.\d+)*)$/iu);
2234
+ if (!versionMatch) {
2235
+ return normalizedQualifier;
2236
+ }
2237
+ const components = String(versionMatch[1] || '').split('.');
2238
+ while (components.length > 1 && components[components.length - 1] === '0') {
2239
+ components.pop();
2240
+ }
2241
+ return `version ${components.join('.')}`;
2242
+ }));
2243
+ }
2244
+ function subjectFramesShareQualifiedIdentity(answerFrame, supportFrame) {
2245
+ if (!subjectFramesShareTail(answerFrame, supportFrame)) {
2246
+ return false;
2247
+ }
2248
+ const answerQualifiers = collectScopeQualifierTokens(`${answerFrame.subject} ${answerFrame.tail}`);
2249
+ const supportQualifiers = collectScopeQualifierTokens(`${supportFrame.subject} ${supportFrame.tail}`);
2250
+ if (answerQualifiers.size <= 0
2251
+ || !Array.from(answerQualifiers).some((qualifier) => supportQualifiers.has(qualifier))) {
2252
+ return false;
2253
+ }
2254
+ const answerFeatures = new Set(answerFrame.subjectFeatures.filter((feature) => !SUBJECT_SCOPE_QUALIFIER_FEATURES.has(feature)));
2255
+ const supportFeatures = new Set(supportFrame.subjectFeatures.filter((feature) => !SUBJECT_SCOPE_QUALIFIER_FEATURES.has(feature)));
2256
+ const sharedCoreFeatureCount = Array.from(answerFeatures).filter((feature) => supportFeatures.has(feature)).length;
2257
+ return sharedCoreFeatureCount >= 2;
2258
+ }
2199
2259
  function subjectFramesEquivalent(answerFrame, supportFrame) {
2200
2260
  const normalizedAnswerSubject = normalizeWhitespace(answerFrame.subject).toLowerCase();
2201
2261
  const normalizedSupportSubject = normalizeWhitespace(supportFrame.subject).toLowerCase();
@@ -2205,7 +2265,8 @@ function subjectFramesEquivalent(answerFrame, supportFrame) {
2205
2265
  if (normalizedAnswerSubject === normalizedSupportSubject) {
2206
2266
  return true;
2207
2267
  }
2208
- return computeFeatureOverlapRatio(answerFrame.subjectFeatures, supportFrame.subjectFeatures) >= 0.75;
2268
+ return computeFeatureOverlapRatio(answerFrame.subjectFeatures, supportFrame.subjectFeatures) >= 0.75
2269
+ || subjectFramesShareQualifiedIdentity(answerFrame, supportFrame);
2209
2270
  }
2210
2271
  function computeSubjectFrameTailOverlap(answerFrame, supportFrame) {
2211
2272
  return computeFeatureOverlapRatio(answerFrame.tailFeatures, supportFrame.tailFeatures);
@@ -2579,7 +2640,7 @@ function evaluateGroundingAlignment(context) {
2579
2640
  const scored = candidates
2580
2641
  .map((candidate) => ({
2581
2642
  label: candidate.label,
2582
- score: computeGroundingAlignmentScore(context.draftAnswer, candidate.text),
2643
+ score: computeGroundingAlignmentScore(stripLearningRoutePresentation(context.draftAnswer), candidate.text),
2583
2644
  }))
2584
2645
  .sort((left, right) => right.score - left.score);
2585
2646
  const best = scored[0] || { label: '', score: 0 };
@@ -2617,7 +2678,8 @@ function ragAnswerRoleIsCovered(context, role) {
2617
2678
  if (!supportText) {
2618
2679
  return true;
2619
2680
  }
2620
- return computeGroundingAlignmentScore(context.draftAnswer, supportText) >= 0.22;
2681
+ return splitDraftAnswerClaims(context.draftAnswer)
2682
+ .some((claim) => computeGroundingAlignmentScore(claim, supportText) >= 0.22);
2621
2683
  }
2622
2684
  const HOW_TO_PROFILE_SIGNAL_RULES = [
2623
2685
  {
@@ -2734,6 +2796,446 @@ function buildRagAnswerCompletenessMessage(result) {
2734
2796
  }
2735
2797
  return `Draft answer missed required RAG evidence roles: ${result.missingRoles.join(', ') || 'none'}; missing profile signals: ${result.missingProfileSignals.join(', ') || 'none'}.`;
2736
2798
  }
2799
+ function preservePlannedGraphAnswerClaims(answer, plan) {
2800
+ if (!plan) {
2801
+ return normalizePublicAnswerSurface(answer);
2802
+ }
2803
+ const plannedStatements = (plan.claims || [])
2804
+ .map((claim) => (0, ragPublicText_1.naturalizeRagPublicEvidenceClause)(String(claim.statement || '')))
2805
+ .filter((statement) => (statement
2806
+ && hasBalancedPublicMathDelimiters(statement)
2807
+ && !(0, ragPublicText_1.shouldRejectPublicEvidenceClause)(statement)));
2808
+ if (plannedStatements.length <= 0) {
2809
+ return normalizeWhitespace(answer);
2810
+ }
2811
+ const normalizedAnswer = normalizeWhitespace(answer);
2812
+ const orderedPlanText = plannedStatements.join('\n\n');
2813
+ const supplementalClauses = (0, ragEvidenceQuality_1.segmentRagEvidenceClauses)((0, ragPublicText_1.naturalizeRagPublicEvidenceClause)(normalizedAnswer))
2814
+ .map((clause) => normalizeWhitespace(clause))
2815
+ .filter((clause) => (clause
2816
+ && hasBalancedPublicMathDelimiters(clause)
2817
+ && !(0, ragPublicText_1.shouldRejectPublicEvidenceClause)(clause)))
2818
+ .filter((clause) => plan?.intent !== 'definition' || (!/\b(?:compared?\s+with|versus|vs\.?|plastic\s+cup|metal\s+cup|对比|比较|差异|区别)\b/iu.test(clause)
2819
+ && !/\b(?:remain(?:s)?\s+(?:internal|outside)|public\s+(?:definition|answer)\s+budget|extra\s+sentence|beyond\s+the\s+public)\b/iu.test(clause)))
2820
+ .filter((clause) => !plannedStatements.some((plannedStatement) => {
2821
+ const clauseKey = clause.toLowerCase();
2822
+ const plannedKey = plannedStatement.toLowerCase();
2823
+ return clauseKey === plannedKey
2824
+ || clauseKey.includes(plannedKey)
2825
+ || plannedKey.includes(clauseKey);
2826
+ }));
2827
+ const supplementalText = plan?.intent === 'definition'
2828
+ ? ''
2829
+ : supplementalClauses.join('\n\n');
2830
+ const useChinese = /[\u3400-\u9fff]/u.test(`${answer} ${plan.answerTaskPlan?.primarySubject || ''}`);
2831
+ return (0, answerTaskPlan_1.formatTaskAwareAnswer)([orderedPlanText, supplementalText], plan.answerTaskPlan, useChinese);
2832
+ }
2833
+ function isIncompletePublicGraphClaim(value) {
2834
+ const normalized = normalizeWhitespace(value);
2835
+ if (normalized.length < (containsCjk(normalized) ? 5 : 16) || /^\d+(?:\.\d+)*[.\u3001)]?$/u.test(normalized)) {
2836
+ return true;
2837
+ }
2838
+ const endsWithTerminalPunctuation = /[.!?\u3002\uFF01\uFF1F]$/u.test(normalized);
2839
+ const endsWithBalancedMath = /(?<!\\)(?:\$\$[\s\S]+?\$\$|\$[^$\n]+\$)$/u.test(normalized);
2840
+ if (!endsWithTerminalPunctuation && !endsWithBalancedMath) {
2841
+ return true;
2842
+ }
2843
+ if (/[::]$|(?:and|or|with|between|from|to|is|are|以及|并且|其中|通常在|范围为|分别为|性能)$/iu.test(normalized)) {
2844
+ return true;
2845
+ }
2846
+ return false;
2847
+ }
2848
+ function isDefinitionComparisonOrArtifactClaim(value) {
2849
+ const normalized = normalizeWhitespace(value);
2850
+ return /\b(?:compare|comparison|contrast|versus|vs\.?|plastic\s+cup|metal\s+cup|ceramic\s+(?:mug|cup)|technologies|context\s+paragraph|local\s+window|technical\s+document|subsequent\s+branch)\b/iu.test(normalized)
2851
+ || /\b(?:validates?\s+(?:the\s+)?source|source\s+anchoring|same\s+clause\s+appears\s+in\s+multiple\s+sections)\b/iu.test(normalized)
2852
+ || /相关技术|比较|对比|塑料杯|金属杯|陶瓷杯|流体容器技术比较|上下文段落|后续分支|本技术文档|技术规格|性能特征|核心概念|我们将从|我们将|本节|本章/u.test(normalized);
2853
+ }
2854
+ function isDefinitionDisambiguationContextStatement(value) {
2855
+ const normalized = normalizeWhitespace(value);
2856
+ if (!normalized) {
2857
+ return false;
2858
+ }
2859
+ const sectionReference = /\b(?:(?:target|selected|matched|active|current|applicable)\s+(?:section|occurrence|version|record|entry|context)|(?:first|second|later|earlier|specific|corresponding)\s+(?:occurrence|section|match|entry))\b/iu.test(normalized);
2860
+ const authoritySignal = /\b(?:controls?|determines?|applies?|guides?|answers?|answer|selected|active|authoritative|canonical|use|used)\b/iu.test(normalized);
2861
+ return sectionReference && authoritySignal;
2862
+ }
2863
+ function isFullDocumentDefinitionNoiseStatement(value) {
2864
+ const normalized = normalizeWhitespace(value);
2865
+ return !normalized
2866
+ || /^\d+(?:\.\d+)*[.、)]?$/u.test(normalized)
2867
+ || /常见用例|应用场景|关键技术规格|技术规格|性能指标|性能特征|统计度量|量化分析|静水压力|环向应力|威布尔|机械可靠性|simple\s+optical\s+lens|use\s+case|application\s+scenario|technical\s+specification|performance\s+(?:metric|characteristic)|quantitative\s+analysis|hydrostatic\s+pressure|hoop\s+stress|weibull|mechanical\s+reliability/iu.test(normalized);
2868
+ }
2869
+ function extractNormalizedPublicMathExpressions(value) {
2870
+ const source = String(value || '');
2871
+ const expressions = [];
2872
+ const displayPattern = /(?<!\\)\$\$([\s\S]*?)(?<!\\)\$\$/gu;
2873
+ for (const match of source.matchAll(displayPattern)) {
2874
+ const expression = normalizeWhitespace(match[1]).replace(/\s+/gu, '');
2875
+ if (expression) {
2876
+ expressions.push(expression);
2877
+ }
2878
+ }
2879
+ const inlineSource = source.replace(displayPattern, '');
2880
+ const inlinePattern = /(?<!\\)\$([^$\n]+?)(?<!\\)\$/gu;
2881
+ for (const match of inlineSource.matchAll(inlinePattern)) {
2882
+ const expression = normalizeWhitespace(match[1]).replace(/\s+/gu, '');
2883
+ if (expression) {
2884
+ expressions.push(expression);
2885
+ }
2886
+ }
2887
+ return Array.from(new Set(expressions));
2888
+ }
2889
+ function isFormulaOnlyPublicGraphClaim(value) {
2890
+ const source = String(value || '');
2891
+ if (extractNormalizedPublicMathExpressions(source).length <= 0) {
2892
+ return false;
2893
+ }
2894
+ const withoutDisplayMath = source.replace(/(?<!\\)\$\$[\s\S]*?(?<!\\)\$\$/gu, '');
2895
+ const prose = withoutDisplayMath
2896
+ .replace(/(?<!\\)\$[^$\n]+?(?<!\\)\$/gu, '')
2897
+ .replace(/[()[\]{}.,;:!?]/gu, '')
2898
+ .trim();
2899
+ return prose.length <= 0;
2900
+ }
2901
+ function formulaOnlyPublicClaimIsAlreadyCovered(candidate, selectedClaims) {
2902
+ if (!isFormulaOnlyPublicGraphClaim(candidate.statement)) {
2903
+ return false;
2904
+ }
2905
+ const candidateExpressions = extractNormalizedPublicMathExpressions(candidate.statement);
2906
+ return candidateExpressions.length > 0 && selectedClaims.some((selectedClaim) => {
2907
+ const selectedExpressions = extractNormalizedPublicMathExpressions(selectedClaim.statement);
2908
+ return candidateExpressions.every((expression) => selectedExpressions.includes(expression));
2909
+ });
2910
+ }
2911
+ function isStandaloneVariableDefinitionClaim(value) {
2912
+ const source = normalizeWhitespace(String(value || ''));
2913
+ const expressions = extractNormalizedPublicMathExpressions(source);
2914
+ if (expressions.length !== 1 || /(?<!\\)\$\$[\s\S]*?(?<!\\)\$\$/u.test(source)) {
2915
+ return false;
2916
+ }
2917
+ const prose = source
2918
+ .replace(/(?<!\\)\$[^$\n]+?(?<!\\)\$/gu, '')
2919
+ .replace(/[()[\]{}.,;:!?]/gu, '')
2920
+ .trim();
2921
+ return /^(?:is|are|was|were|means?|denotes?|represents?|分别是|是|为|表示|指)/iu.test(prose)
2922
+ || /(?:^|\s)(?:is|are|means?|denotes?|represents?|分别是|是|为|表示|指)(?:\s|$)/iu.test(prose);
2923
+ }
2924
+ function isStandaloneVariableGlossaryClaim(value) {
2925
+ const source = normalizeWhitespace(String(value || ''));
2926
+ const expressions = extractNormalizedPublicMathExpressions(source);
2927
+ if (expressions.length < 2 || expressions.some((expression) => expression.includes('='))) {
2928
+ return false;
2929
+ }
2930
+ const variableDefinitions = source.match(/(?<!\\)\$[^$\n]+?(?<!\\)\$\s*(?:is|are|means?|denotes?|represents?|是|为|表示|指)/giu) || [];
2931
+ if (variableDefinitions.length < 2) {
2932
+ return false;
2933
+ }
2934
+ const prose = source
2935
+ .replace(/(?<!\\)\$[^$\n]+?(?<!\\)\$/gu, ' ')
2936
+ .trim();
2937
+ return /^(?:where|in which|here|variables?|the variables|其中|其中的|分别|各变量)/iu.test(prose)
2938
+ || variableDefinitions.length >= 3;
2939
+ }
2940
+ function definitionQueryMatchesVariableGlossary(glossary, message) {
2941
+ const normalizedMessage = normalizeWhitespace(String(message || ''))
2942
+ .replace(/^(?:what\s+is|what'?s|what\s+are|define|definition\s+of|meaning\s+of|什么是|指的是什么|定义|是什么意思)\s*/iu, '')
2943
+ .replace(/[??!!。.;;]+$/u, '')
2944
+ .trim()
2945
+ .toLowerCase();
2946
+ if (!normalizedMessage) {
2947
+ return false;
2948
+ }
2949
+ if (/\b(?:cfl|courant(?:-friedrichs-lewy)?|mesh(?:ing)?|grid|numerical\s+simulation|characteristic\s+velocity|finite\s+(?:volume|element))\b/iu.test(normalizedMessage)
2950
+ || /库朗|数值模拟|网格|特征速度|有限元|有限体积|稳定性/u.test(normalizedMessage)) {
2951
+ return true;
2952
+ }
2953
+ const source = normalizeWhitespace(String(glossary || ''))
2954
+ .toLowerCase()
2955
+ .replace(/\\/gu, '');
2956
+ const queryTokens = normalizedMessage.match(/[\p{L}\p{N}]+/gu) || [];
2957
+ const stopwords = new Set([
2958
+ 'a', 'an', 'the', 'and', 'are', 'be', 'definition', 'define', 'does', 'how',
2959
+ 'is', 'meaning', 'of', 'or', 'what', 'which', 'who', 'with', '这', '个', '是', '的',
2960
+ ]);
2961
+ return queryTokens.some((token) => !stopwords.has(token) && source.includes(token));
2962
+ }
2963
+ function isUnrelatedVariableGlossaryClaim(value, message) {
2964
+ return isStandaloneVariableGlossaryClaim(value)
2965
+ && !definitionQueryMatchesVariableGlossary(value, message);
2966
+ }
2967
+ function publicClaimCarriesCompleteMathContext(value) {
2968
+ const source = normalizeWhitespace(String(value || ''));
2969
+ if (!source
2970
+ || isStandaloneVariableDefinitionClaim(source)
2971
+ || isStandaloneVariableGlossaryClaim(source)) {
2972
+ return false;
2973
+ }
2974
+ const expressions = extractNormalizedPublicMathExpressions(source);
2975
+ return expressions.some((expression) => expression.includes('='));
2976
+ }
2977
+ function publicClaimMathKeys(value) {
2978
+ return extractNormalizedPublicMathExpressions(value)
2979
+ .filter((expression) => expression.includes('='));
2980
+ }
2981
+ function extractConflictPredicate(value) {
2982
+ const normalized = normalizeWhitespace(String(value || '')).replace(/[.!?。!?]+$/u, '');
2983
+ const englishMatch = normalized.match(/^(.+?)\s+(?:is|are|was|were|equals?|remains?|contains?|uses?)\s+(.+)$/iu);
2984
+ if (englishMatch) {
2985
+ return {
2986
+ subject: normalizeWhitespace(englishMatch[1]).toLowerCase(),
2987
+ value: normalizeWhitespace(englishMatch[2]).toLowerCase(),
2988
+ };
2989
+ }
2990
+ const chineseMatch = normalized.match(/^(.+?)(?:是|为|包含|使用|位于)(.+)$/u);
2991
+ if (chineseMatch) {
2992
+ return {
2993
+ subject: normalizeWhitespace(chineseMatch[1]).toLowerCase(),
2994
+ value: normalizeWhitespace(chineseMatch[2]).toLowerCase(),
2995
+ };
2996
+ }
2997
+ return null;
2998
+ }
2999
+ function selectConflictDefinitionClaims(candidates) {
3000
+ const requiredCandidates = candidates.filter(({ claim }) => claim.required);
3001
+ const conflictCandidates = requiredCandidates.filter(({ claim }, candidateIndex) => {
3002
+ const left = extractConflictPredicate(claim.statement);
3003
+ if (!left) {
3004
+ return false;
3005
+ }
3006
+ return requiredCandidates.some(({ claim: otherClaim }, otherIndex) => {
3007
+ if (candidateIndex === otherIndex) {
3008
+ return false;
3009
+ }
3010
+ const right = extractConflictPredicate(otherClaim.statement);
3011
+ return Boolean(right && right.subject === left.subject && right.value !== left.value);
3012
+ });
3013
+ });
3014
+ return conflictCandidates.length >= 2 ? conflictCandidates : [];
3015
+ }
3016
+ function findContextualFormulaClaim(formulaOnlyCandidate, candidates) {
3017
+ if (!isFormulaOnlyPublicGraphClaim(formulaOnlyCandidate.statement)) {
3018
+ return null;
3019
+ }
3020
+ const candidateExpressions = extractNormalizedPublicMathExpressions(formulaOnlyCandidate.statement);
3021
+ if (candidateExpressions.length <= 0) {
3022
+ return null;
3023
+ }
3024
+ return candidates.find((candidate) => {
3025
+ if (candidate.claimId === formulaOnlyCandidate.claimId || isFormulaOnlyPublicGraphClaim(candidate.statement)) {
3026
+ return false;
3027
+ }
3028
+ const contextualExpressions = extractNormalizedPublicMathExpressions(candidate.statement);
3029
+ return candidateExpressions.every((expression) => contextualExpressions.includes(expression));
3030
+ }) || null;
3031
+ }
3032
+ function selectPublicGraphPlanStatements(plan, ragContextPack, message) {
3033
+ const claims = Array.isArray(plan?.claims)
3034
+ ? plan.claims.filter((claim) => hasBalancedPublicMathDelimiters(String(claim.statement || '')))
3035
+ : [];
3036
+ if (plan?.intent !== 'definition') {
3037
+ return claims;
3038
+ }
3039
+ const eligibleClaims = claims
3040
+ .filter((claim) => claim.role !== 'contrast' && claim.role !== 'analogy')
3041
+ .filter((claim) => !/\b(?:compared?\s+with|versus|vs\.?|plastic\s+cup|metal\s+cup|对比|比较|差异|区别)\b/iu.test(String(claim.statement || '')))
3042
+ .filter((claim) => !/\b(?:remain(?:s)?\s+(?:internal|outside)|public\s+(?:definition|answer)\s+budget|extra\s+sentence|beyond\s+the\s+public)\b/iu.test(String(claim.statement || '')))
3043
+ .filter((claim) => !isIncompletePublicGraphClaim(String(claim.statement || '')))
3044
+ .filter((claim) => !isDefinitionComparisonOrArtifactClaim(String(claim.statement || '')))
3045
+ .filter((claim) => !isFullDocumentDefinitionNoiseStatement(String(claim.statement || '')))
3046
+ .filter((claim) => !isUnrelatedVariableGlossaryClaim(String(claim.statement || ''), String(message || '')));
3047
+ const selectedCandidates = [];
3048
+ const selectedClaimIds = new Set();
3049
+ const appendCandidate = (candidate) => {
3050
+ if (selectedClaimIds.has(candidate.claim.claimId)) {
3051
+ return;
3052
+ }
3053
+ if (formulaOnlyPublicClaimIsAlreadyCovered(candidate.claim, selectedCandidates.map((selectedCandidate) => selectedCandidate.claim))) {
3054
+ return;
3055
+ }
3056
+ selectedCandidates.push(candidate);
3057
+ selectedClaimIds.add(candidate.claim.claimId);
3058
+ };
3059
+ eligibleClaims.forEach((claim, index) => {
3060
+ const contextualClaim = findContextualFormulaClaim(claim, eligibleClaims);
3061
+ appendCandidate({ claim: contextualClaim || claim, index });
3062
+ });
3063
+ let selected = selectedCandidates
3064
+ .sort((left, right) => left.index - right.index)
3065
+ .map(({ claim }) => claim);
3066
+ if (ragContextPack?.fragments?.some((fragment) => fragment.role === 'conflict')) {
3067
+ const conflictClaims = selectConflictDefinitionClaims(selectedCandidates);
3068
+ if (conflictClaims.length >= 2) {
3069
+ return conflictClaims
3070
+ .sort((left, right) => left.index - right.index)
3071
+ .map(({ claim }) => claim);
3072
+ }
3073
+ }
3074
+ if (ragContextPack?.sourceBoundary === 'full_document' && selected.length > 0) {
3075
+ const selectedClaimIds = new Set();
3076
+ const boundedCandidates = [];
3077
+ const appendBoundedCandidate = (candidate) => {
3078
+ if (boundedCandidates.length >= 5 || selectedClaimIds.has(candidate.claim.claimId)) {
3079
+ return;
3080
+ }
3081
+ if (formulaOnlyPublicClaimIsAlreadyCovered(candidate.claim, boundedCandidates.map((entry) => entry.claim))) {
3082
+ return;
3083
+ }
3084
+ boundedCandidates.push(candidate);
3085
+ selectedClaimIds.add(candidate.claim.claimId);
3086
+ };
3087
+ const indexedSelected = selected.map((claim, index) => ({ claim, index }));
3088
+ const definitionCandidates = indexedSelected.filter((entry) => (entry.claim.role === 'definition'
3089
+ && !isFormulaOnlyPublicGraphClaim(entry.claim.statement)
3090
+ && !isStandaloneVariableDefinitionClaim(entry.claim.statement)));
3091
+ const leadingDefinition = definitionCandidates.find(entry => extractNormalizedPublicMathExpressions(entry.claim.statement).length === 0)
3092
+ || definitionCandidates[0];
3093
+ if (leadingDefinition) {
3094
+ appendBoundedCandidate(leadingDefinition);
3095
+ }
3096
+ indexedSelected
3097
+ .filter((entry) => isDefinitionDisambiguationContextStatement(entry.claim.statement))
3098
+ .sort((left, right) => right.claim.priority - left.claim.priority || left.index - right.index)
3099
+ .slice(0, 1)
3100
+ .forEach(appendBoundedCandidate);
3101
+ if (isCompoundLearningDefinitionQuery(String(message || ''))) {
3102
+ indexedSelected
3103
+ .filter((entry) => extractNormalizedPublicMathExpressions(entry.claim.statement).length > 0)
3104
+ .filter((entry) => !publicClaimCarriesCompleteMathContext(entry.claim.statement))
3105
+ .filter((entry) => !isStandaloneVariableDefinitionClaim(entry.claim.statement))
3106
+ .sort((left, right) => right.claim.priority - left.claim.priority || left.index - right.index)
3107
+ .slice(0, 1)
3108
+ .forEach(appendBoundedCandidate);
3109
+ }
3110
+ indexedSelected
3111
+ .filter((entry) => ['composition', 'boundary', 'attribute'].includes(entry.claim.role))
3112
+ .filter((entry) => !isUnrelatedVariableGlossaryClaim(entry.claim.statement, String(message || '')))
3113
+ .slice(0, 2)
3114
+ .forEach(appendBoundedCandidate);
3115
+ // Full-document definition evidence often contains a variable glossary
3116
+ // after each equation. Prefer complete equation contexts across distinct
3117
+ // formulas; standalone `$x$ is ...` clauses are useful only as fallback
3118
+ // when no contextual equation is available.
3119
+ const selectedMathKeys = new Set();
3120
+ let selectedMathContextCount = 0;
3121
+ indexedSelected
3122
+ .filter((entry) => publicClaimCarriesCompleteMathContext(entry.claim.statement))
3123
+ .sort((left, right) => (Number(right.claim.role === 'definition')
3124
+ - Number(left.claim.role === 'definition')
3125
+ || right.claim.priority - left.claim.priority
3126
+ || left.index - right.index))
3127
+ .forEach((entry) => {
3128
+ if (selectedMathContextCount >= 2) {
3129
+ return;
3130
+ }
3131
+ const mathKeys = publicClaimMathKeys(entry.claim.statement);
3132
+ if (mathKeys.length <= 0 || mathKeys.some((key) => !selectedMathKeys.has(key))) {
3133
+ appendBoundedCandidate(entry);
3134
+ mathKeys.forEach((key) => selectedMathKeys.add(key));
3135
+ selectedMathContextCount += 1;
3136
+ }
3137
+ });
3138
+ if (boundedCandidates.length <= 1) {
3139
+ indexedSelected
3140
+ .filter((entry) => !isStandaloneVariableDefinitionClaim(entry.claim.statement))
3141
+ .filter((entry) => !isUnrelatedVariableGlossaryClaim(entry.claim.statement, String(message || '')))
3142
+ .filter((entry) => extractNormalizedPublicMathExpressions(entry.claim.statement).length > 0)
3143
+ .forEach((entry) => appendBoundedCandidate(entry));
3144
+ }
3145
+ if (boundedCandidates.length <= 2) {
3146
+ indexedSelected
3147
+ .filter((entry) => entry.claim.role === 'mechanism')
3148
+ .filter((entry) => !isStandaloneVariableDefinitionClaim(entry.claim.statement))
3149
+ .filter((entry) => !isUnrelatedVariableGlossaryClaim(entry.claim.statement, String(message || '')))
3150
+ .filter((entry) => extractNormalizedPublicMathExpressions(entry.claim.statement).length <= 0)
3151
+ .forEach(appendBoundedCandidate);
3152
+ }
3153
+ indexedSelected
3154
+ .filter((entry) => extractNormalizedPublicMathExpressions(entry.claim.statement).length <= 0)
3155
+ .filter((entry) => !isStandaloneVariableDefinitionClaim(entry.claim.statement))
3156
+ .filter((entry) => !isUnrelatedVariableGlossaryClaim(entry.claim.statement, String(message || '')))
3157
+ .filter((entry) => ['composition', 'boundary', 'attribute'].includes(entry.claim.role))
3158
+ .filter((entry) => !isFullDocumentDefinitionNoiseStatement(entry.claim.statement))
3159
+ .forEach(appendBoundedCandidate);
3160
+ // Extraction may classify several independent supported facts as definitions.
3161
+ // Keep those complements within the same cap instead of silently dropping them by role.
3162
+ indexedSelected
3163
+ .filter((entry) => entry.claim.required && entry.claim.role === 'definition')
3164
+ .filter((entry) => !isStandaloneVariableDefinitionClaim(entry.claim.statement))
3165
+ .filter((entry) => !isUnrelatedVariableGlossaryClaim(entry.claim.statement, String(message || '')))
3166
+ .forEach(appendBoundedCandidate);
3167
+ selected = boundedCandidates
3168
+ .sort((left, right) => left.index - right.index)
3169
+ .map(({ claim }) => claim);
3170
+ }
3171
+ return selected;
3172
+ }
3173
+ function projectPublicGraphAnswerPlan(plan, ragContextPack, message) {
3174
+ if (!plan) {
3175
+ return null;
3176
+ }
3177
+ if (plan.intent !== 'definition') {
3178
+ return plan;
3179
+ }
3180
+ const claims = selectPublicGraphPlanStatements(plan, ragContextPack, message);
3181
+ return {
3182
+ ...plan,
3183
+ depth: claims.length <= 2 ? 'compact' : 'standard',
3184
+ leadClaimId: claims[0]?.claimId || '',
3185
+ claims,
3186
+ requiredRoles: Array.from(new Set(claims.map((claim) => claim.role))),
3187
+ };
3188
+ }
3189
+ function createEmptyPublicGraphAnswerPlan(plan) {
3190
+ if (!plan) {
3191
+ return null;
3192
+ }
3193
+ return {
3194
+ ...plan,
3195
+ depth: 'compact',
3196
+ leadClaimId: '',
3197
+ claims: [],
3198
+ requiredRoles: [],
3199
+ };
3200
+ }
3201
+ function isVerifiedRagConflictDisclosurePlan(context, plan) {
3202
+ const fragments = context.ragContextPack?.fragments || [];
3203
+ const conflictFragmentIds = new Set(fragments
3204
+ .filter((fragment) => fragment.role === 'conflict')
3205
+ .map((fragment) => normalizeWhitespace(String(fragment.fragmentId || '')))
3206
+ .filter(Boolean));
3207
+ if (conflictFragmentIds.size <= 0
3208
+ || context.ragSufficiencyReview?.degradationState !== 'conflict'
3209
+ || !plan
3210
+ || plan.claims.length < 2) {
3211
+ return false;
3212
+ }
3213
+ const conflictClaims = plan.claims.filter((claim) => ((claim.evidenceRefs || []).some((evidence) => conflictFragmentIds.has(normalizeWhitespace(String(evidence.evidenceId || ''))))));
3214
+ const nonConflictClaims = plan.claims.filter((claim) => ((claim.evidenceRefs || []).some((evidence) => !conflictFragmentIds.has(normalizeWhitespace(String(evidence.evidenceId || ''))))));
3215
+ return conflictClaims.length > 0
3216
+ && nonConflictClaims.length > 0
3217
+ && plan.claims.every((claim) => (hasBalancedPublicMathDelimiters(String(claim.statement || ''))
3218
+ && (claim.evidenceRefs || []).some((evidence) => normalizeWhitespace(String(evidence.text || '')).length > 0)));
3219
+ }
3220
+ function discloseRagEvidenceConflicts(context, answer) {
3221
+ const conflicts = (context.ragContextPack?.fragments || []).filter(fragment => (fragment.role === 'conflict' && !fragment.truncated && fragment.citationIds.length > 0));
3222
+ if (conflicts.length === 0)
3223
+ return answer;
3224
+ const useChinese = useChineseAnswerLanguage(context);
3225
+ let publicAnswer = answer;
3226
+ for (const fragment of conflicts) {
3227
+ const heading = fragment.text.split('\n', 1)[0];
3228
+ if (heading.startsWith('Conflicting evidence for ')) {
3229
+ publicAnswer = publicAnswer.split(heading).join(useChinese ? '来源中的冲突记录:' : 'Conflicting source observations:');
3230
+ }
3231
+ }
3232
+ // Claim projection can discard supplemental text. The release boundary owns
3233
+ // this conclusion so slim/full and every transport expose the same uncertainty.
3234
+ const notice = useChinese
3235
+ ? '来源中的观测存在冲突,现有证据无法确定哪一项正确。'
3236
+ : 'The source observations conflict; the available evidence does not establish which is correct.';
3237
+ return publicAnswer.startsWith(notice) ? publicAnswer : `${notice}\n\n${publicAnswer}`;
3238
+ }
2737
3239
  function collectCitationBackedRagFragments(context) {
2738
3240
  if (!hasUsableRagEvidenceContext(context)) {
2739
3241
  return [];
@@ -2748,10 +3250,14 @@ function claimLooksLikeReleaseScaffolding(claim) {
2748
3250
  return true;
2749
3251
  }
2750
3252
  return (/\b(?:grounded by|key evidence|citations?|rag context|retrieval|planner)\b/iu.test(normalizedClaim)
3253
+ || /^(?:建议学习路径|推荐学习路径|Suggested learning path)\s*[::]/iu.test(normalizedClaim)
3254
+ || /\b(?:available evidence|evidence coverage)\s+is\s+still\s+partial\b/iu.test(normalizedClaim)
3255
+ || /当前证据覆盖仍然有限|回答只使用已命中的材料|回答只陈述已有材料能够支持的内容/u.test(normalizedClaim)
3256
+ || /^(?:当前图中的关键路径是|The strongest graph path runs through)(?:\s|$)/iu.test(normalizedClaim)
2751
3257
  || INTERNAL_DIAGNOSTIC_FRAGMENTS.some((fragment) => normalizedClaim.includes(fragment)));
2752
3258
  }
2753
3259
  function splitDraftAnswerClaims(answer) {
2754
- return stripMarkdownScaffolding(answer)
3260
+ return stripMarkdownScaffolding(stripLearningRoutePresentation(answer))
2755
3261
  .split(POLARITY_SENTENCE_SPLIT_PATTERN)
2756
3262
  .map((claim) => normalizeWhitespace(claim))
2757
3263
  .filter((claim) => (claim.length >= 8
@@ -2830,6 +3336,12 @@ function evaluateRagClaimCitationSupport(context) {
2830
3336
  }
2831
3337
  const claims = splitDraftAnswerClaims(context.draftAnswer).filter((claim) => (collectClaimSupportFeatures(claim).length >= RAG_CLAIM_CITATION_SUPPORT_MIN_FEATURES));
2832
3338
  const citationBackedFragments = collectCitationBackedRagFragments(context);
3339
+ const citationBackedPlanEvidence = (context.graphAnswerPlan?.claims || [])
3340
+ .flatMap((claim) => claim.evidenceRefs || [])
3341
+ .filter((evidence) => (normalizeWhitespace(String(evidence.text || '')).length > 0
3342
+ && Array.isArray(evidence.citationIds)
3343
+ && evidence.citationIds.some((citationId) => normalizeWhitespace(String(citationId || '')).length > 0)));
3344
+ const citationBackedEvidenceCount = citationBackedFragments.length + citationBackedPlanEvidence.length;
2833
3345
  if (claims.length <= 0) {
2834
3346
  return {
2835
3347
  passed: true,
@@ -2840,7 +3352,7 @@ function evaluateRagClaimCitationSupport(context) {
2840
3352
  citationBackedFragmentCount: citationBackedFragments.length,
2841
3353
  };
2842
3354
  }
2843
- if (citationBackedFragments.length <= 0) {
3355
+ if (citationBackedEvidenceCount <= 0) {
2844
3356
  return {
2845
3357
  passed: false,
2846
3358
  applicable: true,
@@ -2855,6 +3367,7 @@ function evaluateRagClaimCitationSupport(context) {
2855
3367
  normalizeWhitespace(String(fragment.title || '').trim()),
2856
3368
  normalizeWhitespace(String(fragment.text || '').trim()),
2857
3369
  ].filter(Boolean).join(' '))
3370
+ .concat(citationBackedPlanEvidence.map((evidence) => normalizeWhitespace(evidence.text)))
2858
3371
  .filter(Boolean)
2859
3372
  .join(' ');
2860
3373
  const supportFeatures = new Set(collectClaimSupportFeatures(supportText));
@@ -2879,7 +3392,7 @@ function evaluateRagClaimCitationSupport(context) {
2879
3392
  supportedClaimCount,
2880
3393
  weakClaims,
2881
3394
  unsupportedClaims,
2882
- citationBackedFragmentCount: citationBackedFragments.length,
3395
+ citationBackedFragmentCount: citationBackedEvidenceCount,
2883
3396
  };
2884
3397
  }
2885
3398
  function buildRagClaimCitationSupportMessage(result) {
@@ -2920,6 +3433,120 @@ function isMetaDocumentaryAnswer(answer) {
2920
3433
  return (ENGLISH_META_DOCUMENTARY_PATTERN.test(normalizedAnswer)
2921
3434
  || CHINESE_META_DOCUMENTARY_PATTERN.test(normalizedAnswer));
2922
3435
  }
3436
+ function normalizeQuerySubject(value) {
3437
+ return normalizeWhitespace(String(value || ''))
3438
+ .replace(/^["'“”‘’`]+|["'“”‘’`]+$/gu, '')
3439
+ .replace(/^(?:a|an|the)\s+/iu, '')
3440
+ .replace(/[??!!。.;;:,,]+$/u, '')
3441
+ .trim();
3442
+ }
3443
+ function stripDefinitionQuestionPredicate(value) {
3444
+ return normalizeWhitespace(value)
3445
+ .replace(/^(?:(?:a|an|the)\s+)?(?:prerequisite|precondition|requirement|dependency|dependencies)\s+(?:for|of)\s+/iu, '')
3446
+ .replace(/\s+(?:made\s+of|composed\s+of|consists?\s+of|used\s+for|used\s+to|designed\s+for|designed\s+to|intended\s+for|located\s+in|found\s+in|contains?|includes?|depends?\s+on)\b.*$/iu, '')
3447
+ .trim();
3448
+ }
3449
+ function querySubjectFeatures(value) {
3450
+ return collectLexicalFeatures(value)
3451
+ .map((feature) => String(feature || '').trim().toLowerCase())
3452
+ .filter((feature) => feature.length > 0 && !QUERY_SUBJECT_STOPWORDS.has(feature));
3453
+ }
3454
+ function compactSubject(value) {
3455
+ return normalizeQuerySubject(value)
3456
+ .toLowerCase()
3457
+ .replace(/[^\p{L}\p{N}]+/gu, '');
3458
+ }
3459
+ function scoreQuerySubjectAgainstLabel(subject, label) {
3460
+ const normalizedSubject = normalizeQuerySubject(subject);
3461
+ const normalizedLabel = normalizeQuerySubject(label);
3462
+ if (!normalizedSubject || !normalizedLabel) {
3463
+ return 0;
3464
+ }
3465
+ const subjectCompact = compactSubject(normalizedSubject);
3466
+ const labelCompact = compactSubject(normalizedLabel);
3467
+ if (!subjectCompact || !labelCompact) {
3468
+ return 0;
3469
+ }
3470
+ if (subjectCompact === labelCompact) {
3471
+ return 1;
3472
+ }
3473
+ // Parenthetical bilingual titles and qualified headings are valid aliases.
3474
+ if (labelCompact.includes(subjectCompact)) {
3475
+ return 0.95;
3476
+ }
3477
+ const subjectFeatures = querySubjectFeatures(normalizedSubject);
3478
+ const labelFeatures = new Set(querySubjectFeatures(normalizedLabel));
3479
+ if (subjectFeatures.length <= 0 || labelFeatures.size <= 0) {
3480
+ return 0;
3481
+ }
3482
+ const overlap = subjectFeatures.filter((feature) => labelFeatures.has(feature)).length;
3483
+ return Number((overlap / subjectFeatures.length).toFixed(4));
3484
+ }
3485
+ function collectQuerySubjectEvidenceLabels(context) {
3486
+ const labels = [
3487
+ ...(context.ragContextPack?.fragments || [])
3488
+ .filter((fragment) => fragment.role !== 'graph_neighbor_support' && fragment.role !== 'background')
3489
+ .flatMap((fragment) => [fragment.title || '', fragment.sourcePath || '', fragment.documentId || '']),
3490
+ ...context.citations.flatMap((citation) => [citation.title || '', citation.sourcePath || '', citation.documentId || '']),
3491
+ ...context.knowledgePoints.map((point) => point.title || ''),
3492
+ ]
3493
+ .map((label) => normalizeQuerySubject(String(label || '')))
3494
+ .filter(Boolean);
3495
+ return Array.from(new Set(labels));
3496
+ }
3497
+ function evaluateQuerySubjectAlignment(context) {
3498
+ const subject = isDefinitionIntentQuery(context.message)
3499
+ ? normalizeQuerySubject(stripDefinitionQuestionPredicate((0, answerTaskPlan_1.extractDefinitionQuerySubject)(context.message)))
3500
+ : '';
3501
+ if (!subject || querySubjectFeatures(subject).length <= 0) {
3502
+ return {
3503
+ passed: true,
3504
+ applicable: false,
3505
+ subject,
3506
+ subjectFeatures: querySubjectFeatures(subject),
3507
+ matchedLabels: [],
3508
+ bestScore: 0,
3509
+ };
3510
+ }
3511
+ const labels = collectQuerySubjectEvidenceLabels(context);
3512
+ const scoredLabels = labels
3513
+ .map((label) => ({ label, score: scoreQuerySubjectAgainstLabel(subject, label) }))
3514
+ .filter((entry) => entry.score > 0)
3515
+ .sort((left, right) => right.score - left.score);
3516
+ const attributeQuery = /^(.+?)\s+(?:for|of)\s+(.+)$/iu.exec(subject);
3517
+ if ((scoredLabels[0]?.score || 0) < 0.9 && attributeQuery
3518
+ && labels.some(label => scoreQuerySubjectAgainstLabel(attributeQuery[2], label) >= 0.9)) {
3519
+ // A title can name an entity while its body states the requested attribute.
3520
+ // Require both witnesses; sharing an attribute with a different entity is insufficient.
3521
+ const attributeSubject = `${attributeQuery[2]} ${attributeQuery[1]}`;
3522
+ for (const candidate of buildSupportCandidates(context)) {
3523
+ for (const frame of extractSubjectFrames(candidate.text)) {
3524
+ const score = scoreQuerySubjectAgainstLabel(attributeSubject, frame.subject);
3525
+ if (score >= 0.9)
3526
+ scoredLabels.push({ label: `${candidate.label}: ${frame.subject}`, score });
3527
+ }
3528
+ }
3529
+ scoredLabels.sort((left, right) => right.score - left.score);
3530
+ }
3531
+ const bestScore = scoredLabels[0]?.score || 0;
3532
+ return {
3533
+ passed: bestScore >= 0.9,
3534
+ applicable: true,
3535
+ subject,
3536
+ subjectFeatures: querySubjectFeatures(subject),
3537
+ matchedLabels: scoredLabels.filter((entry) => entry.score >= 0.9).map((entry) => entry.label).slice(0, 3),
3538
+ bestScore,
3539
+ };
3540
+ }
3541
+ function buildQuerySubjectAlignmentMessage(result) {
3542
+ if (!result.applicable) {
3543
+ return 'The query did not expose a stable definition subject, so subject alignment stayed conservative.';
3544
+ }
3545
+ if (result.passed) {
3546
+ return `Evidence titles aligned with the requested subject "${result.subject}" (best match ${Math.round(result.bestScore * 100)}%).`;
3547
+ }
3548
+ return `Evidence titles did not establish the requested subject "${result.subject}" (best match ${Math.round(result.bestScore * 100)}%); unrelated entities must not be released as the answer.`;
3549
+ }
2923
3550
  function evaluateQueryIntentAlignment(context) {
2924
3551
  if (!isDefinitionIntentQuery(context.message)) {
2925
3552
  return {
@@ -2945,7 +3572,10 @@ function evaluateQueryIntentAlignment(context) {
2945
3572
  };
2946
3573
  }
2947
3574
  return {
2948
- passed: !isMetaDocumentaryAnswer(context.draftAnswer),
3575
+ // Full mode intentionally emits a report preface and section headers;
3576
+ // those are allowed as long as the answer also contains the grounded
3577
+ // definition frame. Slim mode keeps the stricter documentary guard.
3578
+ passed: context.responseMode === 'full' || !isMetaDocumentaryAnswer(context.draftAnswer),
2949
3579
  applicable: true,
2950
3580
  comparableFrameCount: supportFrames.length,
2951
3581
  supportFrame: primarySupportFrame,
@@ -3284,7 +3914,7 @@ function evaluateStructuredComparisonConsistency(context) {
3284
3914
  };
3285
3915
  }
3286
3916
  function evaluateSubjectConsistency(context) {
3287
- const answerFrames = extractSubjectFrames(context.draftAnswer);
3917
+ const answerFrames = extractSubjectFrames(removeSupportedSourceLabels(context, context.draftAnswer));
3288
3918
  if (answerFrames.length <= 0) {
3289
3919
  return {
3290
3920
  passed: true,
@@ -3638,7 +4268,15 @@ function evaluateStateConsistency(context) {
3638
4268
  };
3639
4269
  }
3640
4270
  function evaluatePolarityConsistency(context) {
3641
- const answerSentences = extractPolaritySentences(context.draftAnswer);
4271
+ const sourceHeadings = new Set([
4272
+ ...context.knowledgePoints.map(point => point.title),
4273
+ ...context.citations.map(citation => citation.title),
4274
+ ...(context.ragContextPack?.fragments || []).flatMap(fragment => [fragment.title || '', ...(fragment.headingPath || [])]),
4275
+ ].map(title => normalizeWhitespace(title).toLowerCase()).filter(Boolean));
4276
+ // A known source title is identity/presentation, not a positive assertion.
4277
+ // Unknown headings remain subject to the same contradiction checks as prose.
4278
+ const omitSourceTitleHeadings = (text) => text.replace(/^ {0,3}#{1,6}[\t ]+(.+?)(?:[\t ]+#+[\t ]*)?$/gmu, (heading, title) => (sourceHeadings.has(normalizeWhitespace(title).toLowerCase()) ? '' : heading));
4279
+ const answerSentences = extractPolaritySentences(omitSourceTitleHeadings(removeSupportedSourceLabels(context, context.draftAnswer)), context.responseMode);
3642
4280
  if (answerSentences.length <= 0) {
3643
4281
  return {
3644
4282
  passed: true,
@@ -3646,7 +4284,7 @@ function evaluatePolarityConsistency(context) {
3646
4284
  conflicts: [],
3647
4285
  };
3648
4286
  }
3649
- const supportSentences = buildSupportCandidates(context).flatMap((candidate) => (extractPolaritySentences(candidate.text).map((sentence) => ({
4287
+ const supportSentences = buildSupportCandidates(context).flatMap((candidate) => (extractPolaritySentences(omitSourceTitleHeadings(candidate.text), context.responseMode).map((sentence) => ({
3650
4288
  ...sentence,
3651
4289
  label: candidate.label,
3652
4290
  }))));
@@ -3812,20 +4450,55 @@ function evaluateTemporalValidityConsistency(context) {
3812
4450
  },
3813
4451
  };
3814
4452
  }
3815
- function checkPublicSurfaceContraction(answer) {
3816
- const normalizedAnswer = String(answer || '');
3817
- if (normalizeWhitespace(normalizedAnswer).length > ANSWER_RELEASE_PUBLIC_CHAR_LIMIT) {
4453
+ function checkPublicSurfaceContraction(answer, responseMode = 'slim') {
4454
+ // Learning-route bullets are an explicit typed deliverable, not internal
4455
+ // diagnostics. Exclude that section from the legacy contraction heuristic.
4456
+ const normalizedAnswer = stripLearningRoutePresentation(String(answer || ''));
4457
+ const forbiddenScaffolding = (/\bGrounded by\b/i.test(normalizedAnswer)
4458
+ || /\bKey evidence\b/i.test(normalizedAnswer)
4459
+ || /\bCitations?:\b/i.test(normalizedAnswer));
4460
+ if (forbiddenScaffolding) {
3818
4461
  return false;
3819
4462
  }
3820
- return !(/\bGrounded by\b/i.test(normalizedAnswer)
3821
- || /\bKey evidence\b/i.test(normalizedAnswer)
3822
- || /\bCitations?:\b/i.test(normalizedAnswer)
3823
- || /\n\s*[-*]\s+/u.test(normalizedAnswer));
4463
+ return responseMode === 'full' || !/\n\s*[-*]\s+/u.test(normalizedAnswer);
4464
+ }
4465
+ function finalPublicAnswerPassesSemanticSafety(context, answer) {
4466
+ const finalContext = {
4467
+ ...context,
4468
+ draftAnswer: String(answer || '').replace(/\r?\n/gu, ' '),
4469
+ };
4470
+ const checks = [
4471
+ ['structured', evaluateStructuredConsistency(finalContext).passed],
4472
+ ['structuredComparison', evaluateStructuredComparisonConsistency(finalContext).passed],
4473
+ ['attribute', evaluateAttributeConsistency(finalContext).passed],
4474
+ ['containment', evaluateContainmentConsistency(finalContext).passed],
4475
+ ['composition', evaluateCompositionConsistency(finalContext).passed],
4476
+ ['purpose', evaluatePurposeConsistency(finalContext).passed],
4477
+ ['dependency', evaluateDependencyConsistency(finalContext).passed],
4478
+ ['location', evaluateLocationConsistency(finalContext).passed],
4479
+ ['subject', evaluateSubjectConsistency(finalContext).passed],
4480
+ ['state', evaluateStateConsistency(finalContext).passed],
4481
+ ['polarity', evaluatePolarityConsistency(finalContext).passed],
4482
+ ['graphCausal', evaluateGraphCausalConsistency(finalContext).passed],
4483
+ ['graphOrder', evaluateGraphOrderConsistency(finalContext).passed],
4484
+ ['graphComparison', evaluateGraphComparisonConsistency(finalContext).passed],
4485
+ ];
4486
+ return checks.every(([, passed]) => passed);
4487
+ }
4488
+ function finalPublicAnswerPassesReleaseContract(context, answer, requireSemanticSafety, responseMode = 'slim') {
4489
+ return hasBalancedPublicMathDelimiters(answer)
4490
+ && checkPublicSurfaceContraction(answer, responseMode)
4491
+ && (!requireSemanticSafety || finalPublicAnswerPassesSemanticSafety(context, answer));
3824
4492
  }
3825
- function buildDecision(groundedEvidenceAvailable, groundingAlignmentPassed, queryIntentAlignmentPassed, structuredConsistencyPassed, structuredComparisonConsistencyPassed, attributeConsistencyPassed, containmentConsistencyPassed, compositionConsistencyPassed, purposeConsistencyPassed, dependencyConsistencyPassed, locationConsistencyPassed, subjectConsistencyPassed, stateConsistencyPassed, polarityConsistencyPassed, graphCausalConsistencyPassed, graphOrderConsistencyPassed, graphComparisonConsistencyPassed, temporalValidityConsistencyPassed, ragAnswerCompletenessPassed, ragClaimCitationSupportPassed, leakedInternalFragments, publicSurfaceContracted) {
4493
+ function buildDecision(groundedEvidenceAvailable, groundingAlignmentPassed, queryIntentAlignmentPassed, querySubjectAlignmentPassed, structuredConsistencyPassed, structuredComparisonConsistencyPassed, attributeConsistencyPassed, containmentConsistencyPassed, compositionConsistencyPassed, purposeConsistencyPassed, dependencyConsistencyPassed, locationConsistencyPassed, subjectConsistencyPassed, stateConsistencyPassed, polarityConsistencyPassed, graphCausalConsistencyPassed, graphOrderConsistencyPassed, graphComparisonConsistencyPassed, temporalValidityConsistencyPassed, ragAnswerCompletenessPassed, ragClaimCitationSupportPassed, graphAnswerPlanCoveragePassed, leakedInternalFragments, publicSurfaceContracted) {
3826
4494
  if (!groundedEvidenceAvailable) {
3827
4495
  return 'abstain';
3828
4496
  }
4497
+ // A grounded answer for the wrong entity is unsafe to revise into shape:
4498
+ // the only valid public outcome is an explicit abstention.
4499
+ if (!querySubjectAlignmentPassed) {
4500
+ return 'abstain';
4501
+ }
3829
4502
  if (!groundingAlignmentPassed
3830
4503
  || !queryIntentAlignmentPassed
3831
4504
  || !structuredConsistencyPassed
@@ -3845,6 +4518,7 @@ function buildDecision(groundedEvidenceAvailable, groundingAlignmentPassed, quer
3845
4518
  || !temporalValidityConsistencyPassed
3846
4519
  || !ragAnswerCompletenessPassed
3847
4520
  || !ragClaimCitationSupportPassed
4521
+ || !graphAnswerPlanCoveragePassed
3848
4522
  || leakedInternalFragments.length > 0
3849
4523
  || !publicSurfaceContracted) {
3850
4524
  return 'revise';
@@ -3862,8 +4536,64 @@ function buildReason(decision, groundedEvidenceAvailable) {
3862
4536
  ? 'Draft answer was downgraded even though some evidence existed.'
3863
4537
  : 'Draft answer lacked grounded evidence, so the public answer was downgraded to a concise abstention.';
3864
4538
  }
4539
+ function evaluateLearningRouteSubjectAlignment(plan) {
4540
+ const taskPlan = plan?.answerTaskPlan;
4541
+ const route = taskPlan?.learningRoute || [];
4542
+ if (!taskPlan?.subtasks.some((subtask) => subtask.kind === 'learning_route')) {
4543
+ return true;
4544
+ }
4545
+ const subjectKey = normalizeWhitespace(taskPlan.primarySubject)
4546
+ .replace(/[^\p{L}\p{N}]+/gu, '')
4547
+ .toLowerCase();
4548
+ if (!subjectKey || route.length < 3) {
4549
+ return false;
4550
+ }
4551
+ return route.some((node) => {
4552
+ const titleKey = normalizeWhitespace(node.title)
4553
+ .replace(/[^\p{L}\p{N}]+/gu, '')
4554
+ .toLowerCase();
4555
+ return node.role === 'core' && Boolean(titleKey && (titleKey === subjectKey
4556
+ || titleKey.includes(subjectKey)
4557
+ || subjectKey.includes(titleKey)));
4558
+ });
4559
+ }
4560
+ function evaluateLearningRouteOrderEvidence(plan) {
4561
+ const taskPlan = plan?.answerTaskPlan;
4562
+ const route = taskPlan?.learningRoute || [];
4563
+ if (!taskPlan?.subtasks.some((subtask) => subtask.kind === 'learning_route')) {
4564
+ return true;
4565
+ }
4566
+ const explicitOrderingPresent = route.some((node) => (node.orderingBasis === 'explicit_prerequisite'
4567
+ || node.orderingBasis === 'explicit_sequence'));
4568
+ return route.length >= 3
4569
+ && route.every((node, index) => node.order === index + 1 && node.evidenceRefs.length > 0)
4570
+ && route.some((node) => node.role === 'core')
4571
+ && (!explicitOrderingPresent || route.some((node) => node.orderingBasis === 'explicit_prerequisite'));
4572
+ }
4573
+ function buildAnswerTaskCoverageMessage(result) {
4574
+ if (!result.applicable) {
4575
+ return 'No compound answer-task contract was active for this request.';
4576
+ }
4577
+ if (result.passed) {
4578
+ return `The public answer covered all ${result.coveredSubtaskIds.length} required answer deliverable(s).`;
4579
+ }
4580
+ return `The public answer is missing required deliverable(s): ${result.missingRequiredSubtaskIds.join(', ')}.`;
4581
+ }
4582
+ function appendAnswerTaskDeliverables(answer, plan, answerLanguage) {
4583
+ const normalizedAnswer = String(answer || '').replace(/\r\n?/gu, '\n').trim();
4584
+ if (!plan || !plan.subtasks.some((subtask) => subtask.kind === 'learning_route') || plan.learningRoute.length <= 0) {
4585
+ return normalizedAnswer;
4586
+ }
4587
+ const useChinese = answerLanguage === 'zh'
4588
+ || (answerLanguage !== 'en' && /[\u3400-\u9fff]/u.test(`${normalizedAnswer} ${plan.primarySubject}`));
4589
+ const routeText = (0, answerTaskPlan_1.formatLearningRouteAnswer)(plan, useChinese);
4590
+ if (!routeText || normalizedAnswer.includes(routeText)) {
4591
+ return normalizedAnswer;
4592
+ }
4593
+ return (0, answerTaskPlan_1.formatTaskAwareAnswer)([normalizedAnswer], plan, useChinese);
4594
+ }
3865
4595
  function reviewAnswerRelease(context) {
3866
- const draftAnswer = normalizeWhitespace(context.draftAnswer);
4596
+ const draftAnswer = normalizePublicAnswerSurface(context.draftAnswer);
3867
4597
  const groundedEvidenceAvailable = context.knowledgePoints.length > 0
3868
4598
  || context.citations.length > 0
3869
4599
  || hasUsableRagEvidenceContext(context);
@@ -3889,6 +4619,19 @@ function reviewAnswerRelease(context) {
3889
4619
  comparableFrameCount: 0,
3890
4620
  supportFrame: null,
3891
4621
  };
4622
+ const querySubjectAlignment = groundedEvidenceAvailable
4623
+ ? evaluateQuerySubjectAlignment({
4624
+ ...context,
4625
+ draftAnswer,
4626
+ })
4627
+ : {
4628
+ passed: true,
4629
+ applicable: false,
4630
+ subject: '',
4631
+ subjectFeatures: [],
4632
+ matchedLabels: [],
4633
+ bestScore: 0,
4634
+ };
3892
4635
  const structuredConsistency = groundedEvidenceAvailable
3893
4636
  ? evaluateStructuredConsistency({
3894
4637
  ...context,
@@ -4066,23 +4809,34 @@ function reviewAnswerRelease(context) {
4066
4809
  unsupportedClaims: [],
4067
4810
  citationBackedFragmentCount: 0,
4068
4811
  };
4069
- const publicSurfaceContracted = checkPublicSurfaceContraction(draftAnswer);
4812
+ const draftAnswerTaskCoverage = groundedEvidenceAvailable
4813
+ ? (0, answerTaskPlan_1.reviewAnswerTaskCoverage)(draftAnswer, context.graphAnswerPlan?.answerTaskPlan)
4814
+ : (0, answerTaskPlan_1.reviewAnswerTaskCoverage)('', null);
4815
+ const publicGraphAnswerPlan = projectPublicGraphAnswerPlan(context.graphAnswerPlan, context.ragContextPack, context.message);
4816
+ const verifiedRagConflictDisclosurePlan = isVerifiedRagConflictDisclosurePlan(context, publicGraphAnswerPlan);
4817
+ const definitionProjectionIntegrityPassed = !(context.graphAnswerPlan?.intent === 'definition'
4818
+ && (context.graphAnswerPlan.claims || []).some((claim) => claim.required)
4819
+ && (publicGraphAnswerPlan?.claims || []).length <= 0);
4820
+ const draftGraphAnswerPlanCoverage = groundedEvidenceAvailable
4821
+ ? (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)(draftAnswer, publicGraphAnswerPlan)
4822
+ : (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)('', null);
4823
+ const publicSurfaceContracted = checkPublicSurfaceContraction(draftAnswer, context.responseMode);
4070
4824
  const graphSupportCount = context.graphContext
4071
4825
  ? ((Array.isArray(context.graphContext.relationSummaries) ? context.graphContext.relationSummaries.length : 0)
4072
4826
  + (Array.isArray(context.graphContext.connectionPaths) ? context.graphContext.connectionPaths.length : 0)
4073
4827
  + (Array.isArray(context.graphContext.supportingAtomIds) ? context.graphContext.supportingAtomIds.length : 0))
4074
4828
  : 0;
4075
4829
  const graphSupportSufficient = context.knowledgePoints.length <= 0 || graphSupportCount > 0 || Boolean(context.graphContext?.anchorAtomId);
4076
- const decision = buildDecision(groundedEvidenceAvailable, groundingAlignment.passed, queryIntentAlignment.passed, structuredConsistency.passed, structuredComparisonConsistency.passed, attributeConsistency.passed, containmentConsistency.passed, compositionConsistency.passed, purposeConsistency.passed, dependencyConsistency.passed, locationConsistency.passed, subjectConsistency.passed, stateConsistency.passed, polarityConsistency.passed, graphCausalConsistency.passed, graphOrderConsistency.passed, graphComparisonConsistency.passed, temporalValidityConsistency.passed, ragAnswerCompleteness.passed, ragClaimCitationSupport.passed, leakedInternalFragments, publicSurfaceContracted);
4830
+ const draftDecision = buildDecision(groundedEvidenceAvailable, groundingAlignment.passed, queryIntentAlignment.passed, querySubjectAlignment.passed, structuredConsistency.passed, structuredComparisonConsistency.passed, attributeConsistency.passed, containmentConsistency.passed, compositionConsistency.passed, purposeConsistency.passed, dependencyConsistency.passed, locationConsistency.passed, subjectConsistency.passed, stateConsistency.passed, polarityConsistency.passed, graphCausalConsistency.passed, graphOrderConsistency.passed, graphComparisonConsistency.passed, temporalValidityConsistency.passed, ragAnswerCompleteness.passed, ragClaimCitationSupport.passed, draftGraphAnswerPlanCoverage.passed, leakedInternalFragments, publicSurfaceContracted);
4077
4831
  const primaryGraphCausalConflict = graphCausalConsistency.conflicts[0];
4078
4832
  const primaryGraphOrderConflict = graphOrderConsistency.conflicts[0];
4079
4833
  const primaryGraphComparisonConflict = graphComparisonConsistency.conflicts[0];
4080
4834
  const primaryStructuredComparisonConflict = structuredComparisonConsistency.conflicts[0];
4081
4835
  const primaryTemporalValidityConflict = temporalValidityConsistency.conflict;
4082
- const publicAnswer = normalizeWhitespace(decision === 'abstain'
4083
- ? buildAbstentionAnswer(context.message, context.usedScope)
4084
- : decision === 'revise'
4085
- ? (primaryTemporalValidityConflict
4836
+ let revisedPublicAnswer = draftDecision === 'abstain'
4837
+ ? buildAbstentionAnswer(context)
4838
+ : draftDecision === 'revise'
4839
+ ? appendAnswerTaskDeliverables((primaryTemporalValidityConflict
4086
4840
  ? buildTemporalValidityRevisionAnswer(context, primaryTemporalValidityConflict)
4087
4841
  : primaryGraphCausalConflict
4088
4842
  ? buildGraphCausalRevisionAnswer(context, primaryGraphCausalConflict)
@@ -4094,8 +4848,133 @@ function reviewAnswerRelease(context) {
4094
4848
  ? buildStructuredComparisonRevisionAnswer(context, primaryStructuredComparisonConflict)
4095
4849
  : (!queryIntentAlignment.passed && queryIntentAlignment.supportFrame)
4096
4850
  ? buildDefinitionIntentRevisionAnswer(context, queryIntentAlignment.supportFrame)
4097
- : buildGroundedRevisionAnswer(context))
4098
- : buildReleasedPublicAnswer(context, draftAnswer));
4851
+ : buildGroundedRevisionAnswer(context)), context.graphAnswerPlan?.answerTaskPlan, context.answerLanguage)
4852
+ : appendAnswerTaskDeliverables(buildReleasedPublicAnswer(context, draftAnswer), context.graphAnswerPlan?.answerTaskPlan, context.answerLanguage);
4853
+ // Every claim-level consistency gate is a semantic safety boundary. A plan may
4854
+ // improve coverage, but must never overwrite a correction from any of them.
4855
+ const requiresSafetyCorrection = !verifiedRagConflictDisclosurePlan && [
4856
+ querySubjectAlignment.passed,
4857
+ groundingAlignment.passed,
4858
+ structuredConsistency.passed,
4859
+ structuredComparisonConsistency.passed,
4860
+ attributeConsistency.passed,
4861
+ containmentConsistency.passed,
4862
+ compositionConsistency.passed,
4863
+ purposeConsistency.passed,
4864
+ dependencyConsistency.passed,
4865
+ locationConsistency.passed,
4866
+ subjectConsistency.passed,
4867
+ stateConsistency.passed,
4868
+ polarityConsistency.passed,
4869
+ graphCausalConsistency.passed,
4870
+ graphOrderConsistency.passed,
4871
+ graphComparisonConsistency.passed,
4872
+ temporalValidityConsistency.passed,
4873
+ ].some((passed) => !passed);
4874
+ if (context.responseMode === 'full'
4875
+ && !requiresSafetyCorrection
4876
+ && leakedInternalFragments.length <= 0
4877
+ && hasBalancedPublicMathDelimiters(draftAnswer)) {
4878
+ // Full mode owns the report surface. A non-safety gate (for example a
4879
+ // slim projection coverage heuristic) must not collapse it back to a
4880
+ // single definition sentence.
4881
+ revisedPublicAnswer = draftAnswer;
4882
+ }
4883
+ const candidatePublicPlanCoverage = groundedEvidenceAvailable
4884
+ ? (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)(revisedPublicAnswer, publicGraphAnswerPlan)
4885
+ : (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)('', null);
4886
+ const requiresDefinitionProjection = Boolean(publicGraphAnswerPlan?.intent === 'definition'
4887
+ && (!candidatePublicPlanCoverage.passed
4888
+ || !hasBalancedPublicMathDelimiters(revisedPublicAnswer)
4889
+ || isDefinitionComparisonOrArtifactClaim(revisedPublicAnswer)));
4890
+ const shouldPreservePublicPlan = Boolean(publicGraphAnswerPlan
4891
+ && definitionProjectionIntegrityPassed
4892
+ && !requiresSafetyCorrection
4893
+ && context.responseMode !== 'full'
4894
+ && (requiresDefinitionProjection
4895
+ || (context.ragContextPack?.fragments || []).length > 0));
4896
+ let effectivePublicGraphAnswerPlan = requiresSafetyCorrection
4897
+ ? createEmptyPublicGraphAnswerPlan(context.graphAnswerPlan)
4898
+ : publicGraphAnswerPlan;
4899
+ let decision = definitionProjectionIntegrityPassed
4900
+ ? draftDecision
4901
+ : 'abstain';
4902
+ if (decision === 'release'
4903
+ && draftAnswerTaskCoverage.applicable
4904
+ && !draftAnswerTaskCoverage.passed) {
4905
+ decision = 'revise';
4906
+ }
4907
+ let publicAnswer = context.responseMode === 'full'
4908
+ ? revisedPublicAnswer
4909
+ : preservePlannedGraphAnswerClaims(revisedPublicAnswer, shouldPreservePublicPlan ? effectivePublicGraphAnswerPlan : null);
4910
+ if (decision !== 'abstain') {
4911
+ publicAnswer = appendAnswerTaskDeliverables(publicAnswer, context.graphAnswerPlan?.answerTaskPlan, context.answerLanguage);
4912
+ }
4913
+ let graphAnswerPlanCoverage = groundedEvidenceAvailable
4914
+ ? (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)(publicAnswer, effectivePublicGraphAnswerPlan)
4915
+ : (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)('', null);
4916
+ if (!definitionProjectionIntegrityPassed) {
4917
+ publicAnswer = buildAbstentionAnswer(context);
4918
+ effectivePublicGraphAnswerPlan = createEmptyPublicGraphAnswerPlan(context.graphAnswerPlan);
4919
+ graphAnswerPlanCoverage = (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)(publicAnswer, effectivePublicGraphAnswerPlan);
4920
+ }
4921
+ if (groundedEvidenceAvailable
4922
+ && effectivePublicGraphAnswerPlan
4923
+ && graphAnswerPlanCoverage.applicable
4924
+ && !graphAnswerPlanCoverage.passed) {
4925
+ if (context.responseMode === 'full') {
4926
+ decision = decision === 'abstain' ? 'abstain' : 'revise';
4927
+ }
4928
+ else {
4929
+ const coveragePreservingAnswer = preservePlannedGraphAnswerClaims('', effectivePublicGraphAnswerPlan);
4930
+ const coveragePreservingReview = (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)(coveragePreservingAnswer, effectivePublicGraphAnswerPlan);
4931
+ if (coveragePreservingReview.passed) {
4932
+ publicAnswer = coveragePreservingAnswer;
4933
+ graphAnswerPlanCoverage = coveragePreservingReview;
4934
+ decision = 'revise';
4935
+ }
4936
+ else {
4937
+ publicAnswer = buildAbstentionAnswer(context);
4938
+ graphAnswerPlanCoverage = (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)(publicAnswer, effectivePublicGraphAnswerPlan);
4939
+ decision = 'abstain';
4940
+ }
4941
+ }
4942
+ }
4943
+ if (decision !== 'abstain') {
4944
+ publicAnswer = discloseRagEvidenceConflicts(context, publicAnswer);
4945
+ }
4946
+ if (decision !== 'abstain'
4947
+ && !finalPublicAnswerPassesReleaseContract(context, publicAnswer, Boolean(shouldPreservePublicPlan
4948
+ && effectivePublicGraphAnswerPlan
4949
+ && effectivePublicGraphAnswerPlan.claims.length > 0
4950
+ && !verifiedRagConflictDisclosurePlan), context.responseMode)) {
4951
+ const correctedAnswer = normalizeWhitespace(revisedPublicAnswer);
4952
+ effectivePublicGraphAnswerPlan = createEmptyPublicGraphAnswerPlan(context.graphAnswerPlan);
4953
+ if (correctedAnswer && finalPublicAnswerPassesReleaseContract(context, correctedAnswer, false, context.responseMode)) {
4954
+ publicAnswer = correctedAnswer;
4955
+ graphAnswerPlanCoverage = (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)(publicAnswer, effectivePublicGraphAnswerPlan);
4956
+ decision = 'revise';
4957
+ }
4958
+ else {
4959
+ publicAnswer = buildAbstentionAnswer(context);
4960
+ graphAnswerPlanCoverage = (0, graphAnswerCoverage_1.reviewGraphAnswerCoverage)(publicAnswer, effectivePublicGraphAnswerPlan);
4961
+ decision = 'abstain';
4962
+ }
4963
+ }
4964
+ const answerTaskCoverage = groundedEvidenceAvailable
4965
+ ? (0, answerTaskPlan_1.reviewAnswerTaskCoverage)(publicAnswer, context.graphAnswerPlan?.answerTaskPlan)
4966
+ : (0, answerTaskPlan_1.reviewAnswerTaskCoverage)('', null);
4967
+ const learningRouteSubjectAlignment = groundedEvidenceAvailable
4968
+ ? evaluateLearningRouteSubjectAlignment(context.graphAnswerPlan)
4969
+ : true;
4970
+ const learningRouteOrderEvidence = groundedEvidenceAvailable
4971
+ ? evaluateLearningRouteOrderEvidence(context.graphAnswerPlan)
4972
+ : true;
4973
+ if (decision === 'release'
4974
+ && answerTaskCoverage.applicable
4975
+ && (!answerTaskCoverage.passed || !learningRouteSubjectAlignment || !learningRouteOrderEvidence)) {
4976
+ decision = 'revise';
4977
+ }
4099
4978
  const abstentionHygienePassed = decision !== 'abstain'
4100
4979
  || (collectLeakedInternalFragments(publicAnswer).length <= 0
4101
4980
  && !/\bretrieval\b/i.test(publicAnswer)
@@ -4137,6 +5016,13 @@ function reviewAnswerRelease(context) {
4137
5016
  : 'No grounded definition frame was available, so intent alignment stayed conservative.')
4138
5017
  : 'No evidence was available, so definition-intent alignment was not evaluated.',
4139
5018
  },
5019
+ {
5020
+ gateId: 'query_subject_alignment',
5021
+ passed: querySubjectAlignment.passed,
5022
+ message: groundedEvidenceAvailable
5023
+ ? buildQuerySubjectAlignmentMessage(querySubjectAlignment)
5024
+ : 'No evidence was available, so query-subject alignment was not evaluated.',
5025
+ },
4140
5026
  {
4141
5027
  gateId: 'claim_structured_consistency',
4142
5028
  passed: structuredConsistency.passed,
@@ -4280,6 +5166,56 @@ function reviewAnswerRelease(context) {
4280
5166
  passed: ragClaimCitationSupport.passed,
4281
5167
  message: buildRagClaimCitationSupportMessage(ragClaimCitationSupport),
4282
5168
  },
5169
+ {
5170
+ gateId: 'graph_answer_plan_coverage',
5171
+ passed: graphAnswerPlanCoverage.passed,
5172
+ message: !graphAnswerPlanCoverage.applicable
5173
+ ? 'No required graph-answer claims were active for this answer.'
5174
+ : graphAnswerPlanCoverage.passed
5175
+ ? `The public answer covered all ${graphAnswerPlanCoverage.requiredClaimIds.length} required graph-answer claim(s).`
5176
+ : `The public answer omitted required graph-answer claims: ${graphAnswerPlanCoverage.missingRequiredClaimIds.join(', ')}.`,
5177
+ },
5178
+ {
5179
+ gateId: 'subtask_coverage',
5180
+ passed: draftAnswerTaskCoverage.passed && answerTaskCoverage.passed,
5181
+ message: draftAnswerTaskCoverage.passed
5182
+ ? buildAnswerTaskCoverageMessage(answerTaskCoverage)
5183
+ : `Draft answer ${buildAnswerTaskCoverageMessage(draftAnswerTaskCoverage).replace(/^The public answer /u, '').replace(/^公共回答/u, '')}`,
5184
+ },
5185
+ {
5186
+ gateId: 'learning_route_subject_alignment',
5187
+ passed: learningRouteSubjectAlignment,
5188
+ message: learningRouteSubjectAlignment
5189
+ ? 'Learning-route nodes retained the requested subject as the core anchor.'
5190
+ : 'Learning-route nodes did not establish the requested subject as a core anchor.',
5191
+ },
5192
+ {
5193
+ gateId: 'learning_route_order_evidence',
5194
+ passed: learningRouteOrderEvidence,
5195
+ message: learningRouteOrderEvidence
5196
+ ? 'Learning-route order is sequential and every node retains evidence references; explicit prerequisite order is used when available.'
5197
+ : 'Learning-route order or evidence references were insufficient for release; unsupported prerequisite order was not inferred.',
5198
+ },
5199
+ {
5200
+ gateId: 'deliverable_completeness',
5201
+ passed: draftAnswerTaskCoverage.passed
5202
+ && answerTaskCoverage.passed
5203
+ && learningRouteSubjectAlignment
5204
+ && learningRouteOrderEvidence,
5205
+ message: draftAnswerTaskCoverage.passed
5206
+ && answerTaskCoverage.passed
5207
+ && learningRouteSubjectAlignment
5208
+ && learningRouteOrderEvidence
5209
+ ? 'All required answer deliverables were present and evidence-bounded.'
5210
+ : 'At least one required answer deliverable was missing or not evidence-bounded.',
5211
+ },
5212
+ {
5213
+ gateId: 'definition_projection_integrity',
5214
+ passed: definitionProjectionIntegrityPassed,
5215
+ message: definitionProjectionIntegrityPassed
5216
+ ? 'Definition-plan projection retained at least one eligible required public claim when projection was required.'
5217
+ : 'Required definition claims projected to zero eligible public claims, so the answer was downgraded to abstention.',
5218
+ },
4283
5219
  {
4284
5220
  gateId: 'public_surface_contraction',
4285
5221
  passed: publicSurfaceContracted,
@@ -4311,6 +5247,11 @@ function reviewAnswerRelease(context) {
4311
5247
  revised: publicAnswer !== draftAnswer,
4312
5248
  originalAnswer: draftAnswer,
4313
5249
  publicAnswer,
5250
+ publicGraphAnswerPlan: effectivePublicGraphAnswerPlan || undefined,
5251
+ auditGraphAnswerPlan: context.graphAnswerPlan || undefined,
5252
+ graphAnswerCoverage: graphAnswerPlanCoverage,
5253
+ answerTaskCoverage,
5254
+ draftAnswerTaskCoverage,
4314
5255
  reason: buildReason(decision, groundedEvidenceAvailable),
4315
5256
  failedGateIds,
4316
5257
  leakedInternalFragments,