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
@@ -5,6 +5,8 @@ exports.normalizeRagContextBudget = normalizeRagContextBudget;
5
5
  exports.estimateRagTokenCount = estimateRagTokenCount;
6
6
  exports.buildRagContextPack = buildRagContextPack;
7
7
  const crypto_1 = require("crypto");
8
+ const graphClaimMatcher_1 = require("./graphClaimMatcher");
9
+ const ragPublicText_1 = require("./ragPublicText");
8
10
  exports.DEFAULT_RAG_CONTEXT_BUDGET = {
9
11
  maxFragments: 16,
10
12
  maxCharsPerFragment: 1600,
@@ -19,6 +21,9 @@ const ROLE_PRIORITY = {
19
21
  background: 5,
20
22
  };
21
23
  function normalizeRagContextBudget(input) {
24
+ const productCapDisabled = input?.productCapDisabled === true;
25
+ const maxFragmentsLimit = productCapDisabled ? 4096 : 512;
26
+ const maxTotalCharsLimit = productCapDisabled ? 512000 : 512000;
22
27
  const numberOrDefault = (value, fallback, min, max) => {
23
28
  const numeric = Number(value);
24
29
  if (!Number.isFinite(numeric)) {
@@ -27,9 +32,15 @@ function normalizeRagContextBudget(input) {
27
32
  return Math.floor(Math.max(min, Math.min(max, numeric)));
28
33
  };
29
34
  return {
30
- maxFragments: numberOrDefault(input?.maxFragments, exports.DEFAULT_RAG_CONTEXT_BUDGET.maxFragments, 1, 200),
35
+ maxFragments: numberOrDefault(input?.maxFragments, exports.DEFAULT_RAG_CONTEXT_BUDGET.maxFragments, 1, maxFragmentsLimit),
31
36
  maxCharsPerFragment: numberOrDefault(input?.maxCharsPerFragment, exports.DEFAULT_RAG_CONTEXT_BUDGET.maxCharsPerFragment, 80, 20000),
32
- maxTotalChars: numberOrDefault(input?.maxTotalChars, exports.DEFAULT_RAG_CONTEXT_BUDGET.maxTotalChars, 120, 100000),
37
+ maxTotalChars: numberOrDefault(input?.maxTotalChars, exports.DEFAULT_RAG_CONTEXT_BUDGET.maxTotalChars, 120, maxTotalCharsLimit),
38
+ ...(productCapDisabled ? {
39
+ productCapDisabled: true,
40
+ runtimeMaxFragments: numberOrDefault(input?.runtimeMaxFragments, input?.maxFragments ?? exports.DEFAULT_RAG_CONTEXT_BUDGET.maxFragments, 1, 4096),
41
+ runtimeMaxCharsPerFragment: numberOrDefault(input?.runtimeMaxCharsPerFragment, input?.maxCharsPerFragment ?? exports.DEFAULT_RAG_CONTEXT_BUDGET.maxCharsPerFragment, 80, 20000),
42
+ runtimeMaxTotalChars: numberOrDefault(input?.runtimeMaxTotalChars, input?.maxTotalChars ?? exports.DEFAULT_RAG_CONTEXT_BUDGET.maxTotalChars, 120, 64 * 1024 * 1024),
43
+ } : {}),
33
44
  };
34
45
  }
35
46
  function estimateRagTokenCount(text) {
@@ -39,7 +50,75 @@ function estimateRagTokenCount(text) {
39
50
  }
40
51
  return Math.max(1, Math.ceil(normalized.length / 4));
41
52
  }
42
- function truncateMiddle(text, maxChars) {
53
+ const QUERY_WINDOW_STOPWORDS = new Set([
54
+ 'and', 'compare', 'contrast', 'difference', 'the', 'versus', 'with',
55
+ ]);
56
+ function queryWindowTerms(query) {
57
+ return Array.from(new Set((String(query || '').toLowerCase().match(/[a-z0-9][a-z0-9_-]{2,}|[\u3400-\u9fff]{2,}/gu) || [])
58
+ .filter((term) => !QUERY_WINDOW_STOPWORDS.has(term))));
59
+ }
60
+ function queryTermCoverage(text, terms) {
61
+ const normalized = text.toLowerCase();
62
+ return terms.reduce((count, term) => count + (normalized.includes(term) ? 1 : 0), 0);
63
+ }
64
+ function queryFeatureCoverage(text, queryFeatures) {
65
+ return (0, graphClaimMatcher_1.semanticFeatures)(text).reduce((count, feature) => count + (queryFeatures.has(feature) ? 1 : 0), 0);
66
+ }
67
+ function queryWindowAnchors(source, terms, queryFeatures) {
68
+ const anchors = new Set();
69
+ const normalizedSource = source.toLowerCase();
70
+ terms.forEach((term) => {
71
+ let matchIndex = normalizedSource.indexOf(term);
72
+ while (matchIndex >= 0) {
73
+ anchors.add(matchIndex);
74
+ matchIndex = normalizedSource.indexOf(term, matchIndex + term.length);
75
+ }
76
+ });
77
+ const clausePattern = /[^\n.!?。!?;;]+(?:[.!?。!?;;]+|$)/gu;
78
+ let clauseMatch;
79
+ while ((clauseMatch = clausePattern.exec(source)) !== null) {
80
+ if (queryFeatureCoverage(clauseMatch[0], queryFeatures) > 0) {
81
+ anchors.add(clauseMatch.index + Math.floor(clauseMatch[0].length / 2));
82
+ }
83
+ }
84
+ return Array.from(anchors).sort((left, right) => left - right);
85
+ }
86
+ function queryCenteredWindow(source, maxChars, query, baseline) {
87
+ const terms = queryWindowTerms(query);
88
+ const queryFeatures = new Set((0, graphClaimMatcher_1.semanticFeatures)(query));
89
+ if (queryFeatures.size < 2) {
90
+ return null;
91
+ }
92
+ const visibleSource = (0, ragPublicText_1.omitFencedMarkdownPayload)(source);
93
+ const markerLength = '\n[...]\n'.length;
94
+ const windowLength = Math.max(16, maxChars - markerLength * 2);
95
+ const visibleBaseline = (0, ragPublicText_1.omitFencedMarkdownPayload)(baseline);
96
+ const baselineFeatureCoverage = queryFeatureCoverage(visibleBaseline, queryFeatures);
97
+ const baselineTermCoverage = queryTermCoverage(visibleBaseline, terms);
98
+ let selected = null;
99
+ for (const anchor of queryWindowAnchors(visibleSource, terms, queryFeatures)) {
100
+ const start = Math.max(0, Math.min(visibleSource.length - windowLength, anchor - Math.floor(windowLength * 0.35)));
101
+ const candidate = visibleSource.slice(start, start + windowLength);
102
+ const featureCoverage = queryFeatureCoverage(candidate, queryFeatures);
103
+ const termCoverage = queryTermCoverage(candidate, terms);
104
+ if (!selected
105
+ || featureCoverage > selected.featureCoverage
106
+ || (featureCoverage === selected.featureCoverage
107
+ && termCoverage > selected.termCoverage)) {
108
+ selected = { start, text: candidate, featureCoverage, termCoverage };
109
+ }
110
+ }
111
+ if (!selected
112
+ || selected.featureCoverage < baselineFeatureCoverage
113
+ || (selected.featureCoverage === baselineFeatureCoverage
114
+ && selected.termCoverage <= baselineTermCoverage)) {
115
+ return null;
116
+ }
117
+ const prefix = selected.start > 0 ? '[...]\n' : '';
118
+ const suffix = selected.start + selected.text.length < visibleSource.length ? '\n[...]' : '';
119
+ return `${prefix}${selected.text.trim()}${suffix}`;
120
+ }
121
+ function truncateMiddle(text, maxChars, query) {
43
122
  const source = String(text || '');
44
123
  if (source.length <= maxChars) {
45
124
  return { text: source, truncated: false };
@@ -51,13 +130,14 @@ function truncateMiddle(text, maxChars) {
51
130
  const available = Math.max(0, maxChars - marker.length);
52
131
  const headLength = Math.ceil(available * 0.6);
53
132
  const tailLength = Math.floor(available * 0.4);
133
+ const baseline = `${source.slice(0, headLength).trimEnd()}${marker}${source.slice(source.length - tailLength).trimStart()}`;
54
134
  return {
55
- text: `${source.slice(0, headLength).trimEnd()}${marker}${source.slice(source.length - tailLength).trimStart()}`,
135
+ text: queryCenteredWindow(source, maxChars, query, baseline) || baseline,
56
136
  truncated: true,
57
137
  };
58
138
  }
59
- function withBudgetedText(fragment, maxChars, reason) {
60
- const truncated = truncateMiddle(fragment.text, maxChars);
139
+ function withBudgetedText(fragment, maxChars, reason, query) {
140
+ const truncated = truncateMiddle(fragment.text, maxChars, query);
61
141
  const text = truncated.text;
62
142
  return {
63
143
  ...fragment,
@@ -71,7 +151,8 @@ function withBudgetedText(fragment, maxChars, reason) {
71
151
  function rolePriority(fragment) {
72
152
  return ROLE_PRIORITY[fragment.role] ?? ROLE_PRIORITY.background;
73
153
  }
74
- function sortFragmentsForBudget(fragments) {
154
+ function sortFragmentsForBudget(fragments, fragmentOrder) {
155
+ const order = new Map((fragmentOrder || []).map((fragmentId, index) => [String(fragmentId), index]));
75
156
  return fragments
76
157
  .map((fragment, index) => ({ fragment, index }))
77
158
  .sort((a, b) => {
@@ -83,33 +164,48 @@ function sortFragmentsForBudget(fragments) {
83
164
  if (Math.abs(scoreDelta) > 0.0001) {
84
165
  return scoreDelta;
85
166
  }
167
+ const conditionedOrderDelta = (order.get(a.fragment.fragmentId) ?? Number.MAX_SAFE_INTEGER)
168
+ - (order.get(b.fragment.fragmentId) ?? Number.MAX_SAFE_INTEGER);
169
+ if (conditionedOrderDelta !== 0) {
170
+ return conditionedOrderDelta;
171
+ }
86
172
  return a.index - b.index;
87
173
  })
88
174
  .map((entry) => entry.fragment);
89
175
  }
90
- function applyContextBudget(fragments, budget, decisions) {
176
+ function applyContextBudget(fragments, budget, decisions, query, fragmentOrder) {
91
177
  const selected = [];
92
178
  let usedChars = 0;
93
- sortFragmentsForBudget(fragments).forEach((fragment) => {
94
- if (selected.length >= budget.maxFragments) {
179
+ const productCapDisabled = budget.productCapDisabled === true;
180
+ const maxFragments = productCapDisabled
181
+ ? budget.runtimeMaxFragments || budget.maxFragments
182
+ : budget.maxFragments;
183
+ const maxCharsPerFragment = productCapDisabled
184
+ ? budget.runtimeMaxCharsPerFragment || budget.maxCharsPerFragment
185
+ : budget.maxCharsPerFragment;
186
+ const maxTotalChars = productCapDisabled
187
+ ? budget.runtimeMaxTotalChars || budget.maxTotalChars
188
+ : budget.maxTotalChars;
189
+ sortFragmentsForBudget(fragments, fragmentOrder).forEach((fragment) => {
190
+ if (selected.length >= maxFragments) {
95
191
  decisions.push({
96
192
  documentId: fragment.documentId,
97
193
  sourcePath: fragment.sourcePath,
98
194
  sourceBoundary: fragment.sourceBoundary,
99
195
  status: 'fragment_dropped',
100
- reason: 'max_fragments_exceeded',
196
+ reason: productCapDisabled ? 'runtime_fragment_limit_exceeded' : 'max_fragments_exceeded',
101
197
  });
102
198
  return;
103
199
  }
104
- let candidate = withBudgetedText(fragment, budget.maxCharsPerFragment, 'max_chars_per_fragment_exceeded');
105
- const remainingChars = budget.maxTotalChars - usedChars;
200
+ let candidate = withBudgetedText(fragment, maxCharsPerFragment, productCapDisabled ? 'runtime_max_chars_per_fragment_exceeded' : 'max_chars_per_fragment_exceeded', query);
201
+ const remainingChars = maxTotalChars - usedChars;
106
202
  if (remainingChars <= 0) {
107
203
  decisions.push({
108
204
  documentId: fragment.documentId,
109
205
  sourcePath: fragment.sourcePath,
110
206
  sourceBoundary: fragment.sourceBoundary,
111
207
  status: 'fragment_dropped',
112
- reason: 'max_total_chars_exceeded',
208
+ reason: productCapDisabled ? 'runtime_total_chars_exceeded' : 'max_total_chars_exceeded',
113
209
  });
114
210
  return;
115
211
  }
@@ -124,7 +220,7 @@ function applyContextBudget(fragments, budget, decisions) {
124
220
  });
125
221
  return;
126
222
  }
127
- candidate = withBudgetedText(candidate, remainingChars, 'max_total_chars_exceeded');
223
+ candidate = withBudgetedText(candidate, remainingChars, productCapDisabled ? 'runtime_total_chars_exceeded' : 'max_total_chars_exceeded', query);
128
224
  }
129
225
  selected.push(candidate);
130
226
  usedChars += candidate.charCount;
@@ -216,6 +312,7 @@ function buildRagContextReplayId(params) {
216
312
  left.status.localeCompare(right.status),
217
313
  left.reason.localeCompare(right.reason),
218
314
  ].find((delta) => delta !== 0) || 0),
315
+ graphConditioning: params.graphConditioning || null,
219
316
  };
220
317
  const digest = (0, crypto_1.createHash)('sha256')
221
318
  .update(JSON.stringify(payload))
@@ -229,7 +326,7 @@ function buildRagContextPack(params) {
229
326
  ? params.sourceDecisions.map((decision) => ({ ...decision }))
230
327
  : [];
231
328
  const fragments = Array.isArray(params.fragments) ? params.fragments.filter(Boolean) : [];
232
- const selectedFragments = applyContextBudget(fragments, budget, decisions);
329
+ const selectedFragments = applyContextBudget(fragments, budget, decisions, params.query, params.fragmentOrder);
233
330
  annotateReadDecisions(decisions, selectedFragments);
234
331
  const totalCharCount = selectedFragments.reduce((sum, fragment) => sum + fragment.charCount, 0);
235
332
  const tokenEstimate = selectedFragments.reduce((sum, fragment) => sum + fragment.tokenEstimate, 0);
@@ -244,6 +341,7 @@ function buildRagContextPack(params) {
244
341
  sourceDecisions: decisions,
245
342
  totalCharCount,
246
343
  tokenEstimate,
344
+ graphConditioning: params.graphConditioning,
247
345
  }),
248
346
  query,
249
347
  generatedAt: String(params.generatedAt || new Date().toISOString()),
@@ -253,5 +351,6 @@ function buildRagContextPack(params) {
253
351
  sourceDecisions: decisions,
254
352
  totalCharCount,
255
353
  tokenEstimate,
354
+ graphConditioning: params.graphConditioning,
256
355
  };
257
356
  }
@@ -117,6 +117,61 @@ describe('buildRagContextPack', () => {
117
117
  }),
118
118
  ]));
119
119
  });
120
+ test('centers the bounded window on query terms when a later comparison clause would otherwise be dropped', () => {
121
+ const longText = [
122
+ 'Opening definition: a water glass is a transparent vessel.',
123
+ 'Background context '.repeat(28),
124
+ 'Comparison: a plastic cup is lightweight and less rigid than a water glass.',
125
+ 'Terminal qualifier: the comparison remains limited to the scoped note.',
126
+ ].join('\n');
127
+ const pack = (0, ragContextPack_1.buildRagContextPack)({
128
+ query: 'compare water glass and plastic cup',
129
+ fragments: [
130
+ makeFragment({
131
+ role: 'parent_context',
132
+ text: longText,
133
+ sourceBoundary: 'full_document',
134
+ }),
135
+ ],
136
+ sourceDecisions: [],
137
+ budget: {
138
+ maxFragments: 3,
139
+ maxCharsPerFragment: 180,
140
+ maxTotalChars: 220,
141
+ },
142
+ generatedAt: '2026-07-05T00:00:00.000Z',
143
+ });
144
+ expect(pack.fragments[0].text).toContain('plastic cup');
145
+ expect(pack.fragments[0].text).toContain('water glass');
146
+ });
147
+ test('prefers multilingual comparison prose outside fenced renderer payloads', () => {
148
+ const longText = [
149
+ 'Opening definition: a water glass is a transparent vessel.',
150
+ 'Background context '.repeat(20),
151
+ '```mermaid',
152
+ 'graph LR',
153
+ 'A[Water Glass] --> B[Plastic Cup PET]',
154
+ '```',
155
+ '相同尺寸下,PET塑料杯比玻璃水杯更轻、更有韧性,但刚度和透明度较低。',
156
+ 'Terminal qualifier: the comparison remains limited to the scoped note.',
157
+ ].join('\n');
158
+ const pack = (0, ragContextPack_1.buildRagContextPack)({
159
+ query: 'compare water glass and plastic cup',
160
+ fragments: [makeFragment({
161
+ role: 'parent_context',
162
+ text: longText,
163
+ sourceBoundary: 'full_document',
164
+ })],
165
+ sourceDecisions: [],
166
+ budget: {
167
+ maxFragments: 3,
168
+ maxCharsPerFragment: 180,
169
+ maxTotalChars: 220,
170
+ },
171
+ });
172
+ expect(pack.fragments[0].text).toContain('PET塑料杯');
173
+ expect(pack.fragments[0].text).not.toContain('graph LR');
174
+ });
120
175
  test('uses role priority so weak background cannot displace direct or graph evidence', () => {
121
176
  const pack = (0, ragContextPack_1.buildRagContextPack)({
122
177
  query: 'what is water glass?',
@@ -157,4 +212,100 @@ describe('buildRagContextPack', () => {
157
212
  }),
158
213
  ]));
159
214
  });
215
+ test('uses graph-conditioned order only as a tie-break inside the same role and score', () => {
216
+ const pack = (0, ragContextPack_1.buildRagContextPack)({
217
+ query: 'what is water glass?',
218
+ fragments: [
219
+ makeFragment({ fragmentId: 'graph-unmatched', role: 'graph_neighbor_support', score: 0.6 }),
220
+ makeFragment({ fragmentId: 'graph-matched', role: 'graph_neighbor_support', score: 0.6 }),
221
+ makeFragment({ fragmentId: 'direct', role: 'direct_support', score: 0.4 }),
222
+ ],
223
+ fragmentOrder: ['graph-matched', 'graph-unmatched', 'direct'],
224
+ sourceDecisions: [],
225
+ budget: {
226
+ maxFragments: 3,
227
+ maxCharsPerFragment: 200,
228
+ maxTotalChars: 600,
229
+ },
230
+ });
231
+ expect(pack.fragments.map((fragment) => fragment.fragmentId)).toEqual([
232
+ 'direct',
233
+ 'graph-matched',
234
+ 'graph-unmatched',
235
+ ]);
236
+ });
237
+ test('unbounded product mode ignores product caps while retaining explicit runtime limits', () => {
238
+ const pack = (0, ragContextPack_1.buildRagContextPack)({
239
+ query: 'what is water glass?',
240
+ fragments: Array.from({ length: 4 }, (_, index) => makeFragment({
241
+ fragmentId: `unbounded_${index}`,
242
+ role: 'parent_context',
243
+ text: `Section ${index}: ${'water glass evidence '.repeat(12)}`,
244
+ sourceBoundary: 'full_document',
245
+ })),
246
+ sourceDecisions: [],
247
+ budget: {
248
+ maxFragments: 1,
249
+ maxCharsPerFragment: 100,
250
+ maxTotalChars: 120,
251
+ productCapDisabled: true,
252
+ runtimeMaxFragments: 8,
253
+ runtimeMaxCharsPerFragment: 900,
254
+ runtimeMaxTotalChars: 10000,
255
+ },
256
+ });
257
+ expect(pack.fragments).toHaveLength(4);
258
+ expect(pack.budget.productCapDisabled).toBe(true);
259
+ expect(pack.sourceDecisions).not.toEqual(expect.arrayContaining([
260
+ expect.objectContaining({ reason: 'max_fragments_exceeded' }),
261
+ ]));
262
+ });
263
+ test('preserves the configured max tier instead of applying the legacy 200-fragment/100k cap', () => {
264
+ const pack = (0, ragContextPack_1.buildRagContextPack)({
265
+ query: 'max budget probe',
266
+ fragments: Array.from({ length: 260 }, (_, index) => makeFragment({
267
+ fragmentId: `max_tier_${index}`,
268
+ role: 'direct_support',
269
+ text: `Max tier evidence ${index}.`,
270
+ sourceBoundary: 'full_document',
271
+ })),
272
+ sourceDecisions: [],
273
+ budget: {
274
+ maxFragments: 256,
275
+ maxCharsPerFragment: 16000,
276
+ maxTotalChars: 256000,
277
+ },
278
+ });
279
+ expect(pack.budget.maxFragments).toBe(256);
280
+ expect(pack.budget.maxTotalChars).toBe(256000);
281
+ expect(pack.fragments).toHaveLength(256);
282
+ });
283
+ test('unbounded mode reports a runtime fragment stop instead of silently dropping content', () => {
284
+ const pack = (0, ragContextPack_1.buildRagContextPack)({
285
+ query: 'what is water glass?',
286
+ fragments: Array.from({ length: 4 }, (_, index) => makeFragment({
287
+ fragmentId: `runtime_limited_${index}`,
288
+ role: 'parent_context',
289
+ text: `Runtime section ${index}: water glass evidence.`,
290
+ sourceBoundary: 'full_document',
291
+ })),
292
+ sourceDecisions: [],
293
+ budget: {
294
+ maxFragments: 1,
295
+ maxCharsPerFragment: 100,
296
+ maxTotalChars: 120,
297
+ productCapDisabled: true,
298
+ runtimeMaxFragments: 2,
299
+ runtimeMaxCharsPerFragment: 900,
300
+ runtimeMaxTotalChars: 10000,
301
+ },
302
+ });
303
+ expect(pack.fragments).toHaveLength(2);
304
+ expect(pack.sourceDecisions).toEqual(expect.arrayContaining([
305
+ expect.objectContaining({
306
+ status: 'fragment_dropped',
307
+ reason: 'runtime_fragment_limit_exceeded',
308
+ }),
309
+ ]));
310
+ });
160
311
  });
@@ -0,0 +1,96 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.iterateRagEvidenceClauses = iterateRagEvidenceClauses;
4
+ exports.segmentRagEvidenceClauses = segmentRagEvidenceClauses;
5
+ exports.scoreRagEvidenceClause = scoreRagEvidenceClause;
6
+ function normalizeWhitespace(value) {
7
+ return String(value || '').replace(/\s+/gu, ' ').trim();
8
+ }
9
+ function isClauseBoundary(source, index) {
10
+ const character = source[index];
11
+ if (/\r|\n|;/u.test(character)) {
12
+ return true;
13
+ }
14
+ if (character === '.' && /[\d\s=+\-(]/u.test(source[index - 1] || ' ') && /\d/u.test(source[index + 1] || '')) {
15
+ return false;
16
+ }
17
+ return /[.!?。!?;]/u.test(character);
18
+ }
19
+ /**
20
+ * Splits source evidence at discourse boundaries while retaining decimal numbers and
21
+ * mathematical notation. The caller still owns filtering and public-text naturalization.
22
+ */
23
+ function* iterateRagEvidenceClauses(value) {
24
+ const source = String(value || '');
25
+ if (!source) {
26
+ return;
27
+ }
28
+ let start = 0;
29
+ for (let index = 0; index < source.length; index += 1) {
30
+ if (!isClauseBoundary(source, index)) {
31
+ continue;
32
+ }
33
+ const includeBoundary = /[.!?。!?]/u.test(source[index]);
34
+ const clause = normalizeWhitespace(source.slice(start, includeBoundary ? index + 1 : index));
35
+ if (clause) {
36
+ yield clause;
37
+ }
38
+ start = index + 1;
39
+ }
40
+ const remainder = normalizeWhitespace(source.slice(start));
41
+ if (remainder) {
42
+ yield remainder;
43
+ }
44
+ }
45
+ function segmentRagEvidenceClauses(value) {
46
+ return Array.from(iterateRagEvidenceClauses(value));
47
+ }
48
+ function delimiterBalance(value) {
49
+ const pairs = { '(': ')', '[': ']', '{': '}', '(': ')', '[': ']', '{': '}' };
50
+ const closing = new Set(Object.values(pairs));
51
+ const stack = [];
52
+ for (const character of value) {
53
+ if (pairs[character]) {
54
+ stack.push(pairs[character]);
55
+ }
56
+ else if (closing.has(character) && stack.pop() !== character) {
57
+ return false;
58
+ }
59
+ }
60
+ return stack.length === 0;
61
+ }
62
+ function mathDensity(value) {
63
+ const mathTokens = value.match(/(?:\b\d+(?:\.\d+)?\b|[=<>±×÷∑∫√]|\b(?:sin|cos|log|exp)\b)/giu) || [];
64
+ return Math.min(1, mathTokens.length / Math.max(6, value.length / 24));
65
+ }
66
+ function documentaryPenalty(value) {
67
+ return /\b(?:this (?:document|section|table)|the following|as requested|we will|must follow|ignore this)\b/iu.test(value)
68
+ || /(?:本文档|下表|如下|按照您的要求|忽略本节)/u.test(value)
69
+ ? 0.35
70
+ : 0;
71
+ }
72
+ function scoreRagEvidenceClause(value) {
73
+ const clause = normalizeWhitespace(value);
74
+ const hasTerminalPunctuation = /[.!?。!?]$/u.test(clause);
75
+ const hasBalancedDelimiters = delimiterBalance(clause);
76
+ const density = mathDensity(clause);
77
+ const penalty = documentaryPenalty(clause);
78
+ const lengthScore = clause.length < 8
79
+ ? -0.3
80
+ : clause.length <= 420
81
+ ? 0.25
82
+ : -Math.min(0.25, (clause.length - 420) / 2400);
83
+ const score = Math.max(0, Math.min(1, 0.35
84
+ + (hasTerminalPunctuation ? 0.15 : -0.08)
85
+ + (hasBalancedDelimiters ? 0.15 : -0.3)
86
+ + Math.min(0.2, density * 0.2)
87
+ + lengthScore
88
+ - penalty));
89
+ return {
90
+ score: Number(score.toFixed(4)),
91
+ hasTerminalPunctuation,
92
+ hasBalancedDelimiters,
93
+ mathDensity: Number(density.toFixed(4)),
94
+ documentaryPenalty: penalty,
95
+ };
96
+ }
@@ -0,0 +1,29 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ const ragEvidenceQuality_1 = require("./ragEvidenceQuality");
4
+ describe('ragEvidenceQuality', () => {
5
+ test('segments prose without splitting decimal mathematical values', () => {
6
+ expect((0, ragEvidenceQuality_1.segmentRagEvidenceClauses)('The coefficient is 0.75; heat transfer follows the wall.')).toEqual([
7
+ 'The coefficient is 0.75',
8
+ 'heat transfer follows the wall.',
9
+ ]);
10
+ });
11
+ test('scores complete balanced mathematical evidence above documentary prose', () => {
12
+ const evidence = (0, ragEvidenceQuality_1.scoreRagEvidenceClause)('The thermal flux is q = kA(T1 - T2) / L, so thickness changes the exchange rate.');
13
+ const documentary = (0, ragEvidenceQuality_1.scoreRagEvidenceClause)('This document will explain the following table as requested.');
14
+ expect(evidence.hasBalancedDelimiters).toBe(true);
15
+ expect(evidence.mathDensity).toBeGreaterThan(0);
16
+ expect(evidence.score).toBeGreaterThan(documentary.score);
17
+ expect(documentary.documentaryPenalty).toBeGreaterThan(0);
18
+ });
19
+ test('preserves leading decimal measurements without joining numbered sentences', () => {
20
+ expect((0, ragEvidenceQuality_1.segmentRagEvidenceClauses)('The offset is -.25 ms. The duration is .5 s; 1. Next observation.')).toEqual([
21
+ 'The offset is -.25 ms.', 'The duration is .5 s', '1.', 'Next observation.',
22
+ ]);
23
+ });
24
+ test('penalizes unbalanced mathematical fragments', () => {
25
+ const quality = (0, ragEvidenceQuality_1.scoreRagEvidenceClause)('The flux is q = kA(T1 - T2 / L.');
26
+ expect(quality.hasBalancedDelimiters).toBe(false);
27
+ expect(quality.score).toBeLessThan(0.5);
28
+ });
29
+ });
@@ -1,14 +1,65 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.omitFencedMarkdownPayload = omitFencedMarkdownPayload;
4
+ exports.extractPublicMermaidEvidenceLabels = extractPublicMermaidEvidenceLabels;
3
5
  exports.naturalizeRagPublicEvidenceClause = naturalizeRagPublicEvidenceClause;
6
+ exports.shouldRejectPublicEvidenceClause = shouldRejectPublicEvidenceClause;
4
7
  exports.shouldRejectCompareProcedureEvidenceClause = shouldRejectCompareProcedureEvidenceClause;
8
+ const answerTaskPlan_1 = require("./answerTaskPlan");
5
9
  function normalizeWhitespace(value) {
6
10
  return String(value || '').replace(/\s+/g, ' ').trim();
7
11
  }
8
- const LEADING_SOURCE_LABEL_PATTERN = /^(?:prerequisites?|preconditions?|requirements?|background|context|mechanism|reasoning boundary|downstream checks?|downstream consequences?|failure modes?|mitigation(?: neighbor)?|graph caveat|caveat|evidence|summary|note)\s*[:\uFF1A-]\s*/iu;
12
+ function omitFencedMarkdownPayload(value) {
13
+ return String(value || '')
14
+ .replace(/```[\s\S]*?```/gu, ' ')
15
+ .replace(/```[\s\S]*$/u, ' ');
16
+ }
17
+ function normalizeMermaidLabel(value) {
18
+ return normalizeWhitespace(String(value || '')
19
+ .replace(/<br\s*\/?>/giu, ' ')
20
+ .replace(/\\n/gu, ' ')
21
+ .replace(/["`*_~#|{}]/gu, ' '));
22
+ }
23
+ /**
24
+ * Converts diagram node labels into evidence text while keeping Mermaid syntax out of public answers.
25
+ */
26
+ function extractPublicMermaidEvidenceLabels(value) {
27
+ const labels = [];
28
+ const append = (candidate) => {
29
+ const label = normalizeMermaidLabel(candidate);
30
+ if (label && !labels.includes(label)) {
31
+ labels.push(label);
32
+ }
33
+ };
34
+ const source = String(value || '');
35
+ const fences = source.matchAll(/```mermaid\s*([\s\S]*?)(?:```|$)/giu);
36
+ for (const fence of fences) {
37
+ const body = String(fence[1] || '');
38
+ body.replace(/\[([^\]]+)\]/gu, (_match, label) => {
39
+ append(label);
40
+ return '';
41
+ });
42
+ body.replace(/"([^"]+)"/gu, (_match, label) => {
43
+ append(label);
44
+ return '';
45
+ });
46
+ }
47
+ return labels;
48
+ }
49
+ const LEADING_SOURCE_LABEL_PATTERN = /^(?:(?:prerequisites?|preconditions?|requirements?|background|context|mechanism|reasoning boundary|downstream checks?|downstream consequences?|failure modes?|mitigation(?: neighbor)?|graph caveat|caveat|evidence|summary|note|material science)\s*[:\uFF1A-]\s*|(?:核心概念(?:及其数学基础)?|材料科学|技术参数)(?:\s*[:\uFF1A-]\s*|\s+))/iu;
9
50
  const PRESERVED_LEADING_LABEL_PATTERN = /^(?:step|phase|stage|section)\s*\d+\s*[:\uFF1A-]/iu;
10
51
  function naturalizeRagPublicEvidenceClause(value) {
11
- let normalized = normalizeWhitespace(value);
52
+ let normalized = normalizeWhitespace(omitFencedMarkdownPayload(value)
53
+ .replace(/^\s*[-+*]\s+/gmu, '')
54
+ .replace(/\s+\*\s+(?=\$|\*\*|[\u3400-\u9fff])/gu, ' ')
55
+ .replace(/\*\*([^*]+)\*\*/gu, '$1')
56
+ .replace(/([+\-]\/)\s+(?=\d)/gu, '$1')
57
+ .replace(/(\d)\.\s+(?=\d)/gu, '$1.')
58
+ .replace(/^(?:#{1,6})\s+/u, ''));
59
+ const tableStart = normalized.search(/\s\|\s*[^|]+\s*\|/u);
60
+ if (tableStart >= 0) {
61
+ normalized = normalizeWhitespace(normalized.slice(0, tableStart));
62
+ }
12
63
  if (!normalized || PRESERVED_LEADING_LABEL_PATTERN.test(normalized)) {
13
64
  return normalized;
14
65
  }
@@ -24,15 +75,27 @@ function naturalizeRagPublicEvidenceClause(value) {
24
75
  }
25
76
  return normalized;
26
77
  }
78
+ function shouldRejectPublicEvidenceClause(value) {
79
+ const normalized = normalizeWhitespace(value);
80
+ // A bare wiki link identifies a note, but supplies no factual statement for answer coverage.
81
+ return /^(?:(?:\d+[.)]|[-*+])?\s*\[\[[^\]]+\]\][\s,;、,;.!。]*)+$/u.test(normalized)
82
+ || /\b(?:distractor|must not guide|do not use|ignore this (?:section|evidence)|must (?:compare|resolve)|before publishing)\b/iu.test(normalized)
83
+ || /\b(?:this|the)\s+(?:clause|sentence|paragraph)\b[^.!?]{0,120}\bpublic\s+(?:definition|answer)\s+budget\b/iu.test(normalized)
84
+ || /\b(?:the following table|the table below)\b/iu.test(normalized)
85
+ || /(?:下表|以下表格|表中)(?:列出|展示|给出|包含)/u.test(normalized)
86
+ || /(?:参数\s*\(Parameter\)|Parameter).{0,160}(?:单位\s*\(Unit\)|Unit).{0,160}:---/iu.test(normalized)
87
+ || /(?:遵从您的指示|所有推理过程|最终输出|仅基于标题|根据您的要求生成|本技术文档旨在)/u.test(normalized);
88
+ }
27
89
  function shouldRejectCompareProcedureEvidenceClause(value, query) {
28
90
  const normalizedClause = normalizeWhitespace(value).toLowerCase();
29
91
  const normalizedQuery = normalizeWhitespace(query).toLowerCase();
30
92
  if (!normalizedClause) {
31
93
  return false;
32
94
  }
33
- if (/\b(?:procedure|procedural|workflow|runbook|steps?|step\s*\d+|sequence)\b/u.test(normalizedQuery)) {
95
+ if ((0, answerTaskPlan_1.isProcedureRequest)(normalizedQuery)) {
34
96
  return false;
35
97
  }
36
- return /\b(?:procedure|workflow|runbook|steps?|step\s*\d+)\b/u.test(normalizedClause)
98
+ return /^(?:(?:procedure|workflow|runbook)\s*[:\uFF1A-]|(?:step\s*\d+|steps?|步骤\s*\d+)\b)/u.test(normalizedClause)
99
+ || /\b(?:follow|execute|perform)\s+(?:this|the following)\s+(?:procedure|workflow|steps?)\b/u.test(normalizedClause)
37
100
  || /\bprocedural\b.{0,80}\bsequence\b/u.test(normalizedClause);
38
101
  }