keywordgraph-mcp-server 1.8.4 → 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 (220) hide show
  1. package/README.md +118 -55
  2. package/dist/api/client.d.ts +10 -0
  3. package/dist/api/client.d.ts.map +1 -1
  4. package/dist/api/client.js +42 -0
  5. package/dist/api/client.js.map +1 -1
  6. package/dist/config/brand.d.ts.map +1 -1
  7. package/dist/config/brand.js +2 -0
  8. package/dist/config/brand.js.map +1 -1
  9. package/dist/config/index.d.ts +1 -1
  10. package/dist/config/index.d.ts.map +1 -1
  11. package/dist/config/index.js +4 -1
  12. package/dist/config/index.js.map +1 -1
  13. package/dist/http-server.js +66 -10
  14. package/dist/http-server.js.map +1 -1
  15. package/dist/index.d.ts +9 -1
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +43 -9
  18. package/dist/index.js.map +1 -1
  19. package/dist/instructions.d.ts.map +1 -1
  20. package/dist/instructions.js +13 -4
  21. package/dist/instructions.js.map +1 -1
  22. package/dist/resources/about.d.ts.map +1 -1
  23. package/dist/resources/about.js +1 -0
  24. package/dist/resources/about.js.map +1 -1
  25. package/dist/schemas/index.d.ts +338 -22
  26. package/dist/schemas/index.d.ts.map +1 -1
  27. package/dist/schemas/index.js +182 -5
  28. package/dist/schemas/index.js.map +1 -1
  29. package/dist/tools/addMemory.d.ts +11 -8
  30. package/dist/tools/addMemory.d.ts.map +1 -1
  31. package/dist/tools/addMemory.js +54 -3
  32. package/dist/tools/addMemory.js.map +1 -1
  33. package/dist/tools/addProjectLearnings.d.ts +1 -0
  34. package/dist/tools/addProjectLearnings.d.ts.map +1 -1
  35. package/dist/tools/addProjectLearnings.js +19 -168
  36. package/dist/tools/addProjectLearnings.js.map +1 -1
  37. package/dist/tools/analyzeExistingGraph.d.ts +2 -0
  38. package/dist/tools/analyzeExistingGraph.d.ts.map +1 -1
  39. package/dist/tools/analyzeExistingGraph.js +3 -1
  40. package/dist/tools/analyzeExistingGraph.js.map +1 -1
  41. package/dist/tools/analyzeLlmResults.d.ts +2 -0
  42. package/dist/tools/analyzeLlmResults.d.ts.map +1 -1
  43. package/dist/tools/analyzeLlmResults.js +2 -0
  44. package/dist/tools/analyzeLlmResults.js.map +1 -1
  45. package/dist/tools/analyzeText.d.ts +3 -0
  46. package/dist/tools/analyzeText.d.ts.map +1 -1
  47. package/dist/tools/analyzeText.js +3 -1
  48. package/dist/tools/analyzeText.js.map +1 -1
  49. package/dist/tools/analyzeTextSignature.d.ts +39 -0
  50. package/dist/tools/analyzeTextSignature.d.ts.map +1 -0
  51. package/dist/tools/analyzeTextSignature.js +83 -0
  52. package/dist/tools/analyzeTextSignature.js.map +1 -0
  53. package/dist/tools/createKnowledgeGraph.d.ts +3 -0
  54. package/dist/tools/createKnowledgeGraph.d.ts.map +1 -1
  55. package/dist/tools/createKnowledgeGraph.js +3 -1
  56. package/dist/tools/createKnowledgeGraph.js.map +1 -1
  57. package/dist/tools/developConceptualBridges.d.ts +2 -0
  58. package/dist/tools/developConceptualBridges.d.ts.map +1 -1
  59. package/dist/tools/developConceptualBridges.js +1 -0
  60. package/dist/tools/developConceptualBridges.js.map +1 -1
  61. package/dist/tools/developLatentTopics.d.ts +2 -0
  62. package/dist/tools/developLatentTopics.d.ts.map +1 -1
  63. package/dist/tools/developLatentTopics.js +1 -0
  64. package/dist/tools/developLatentTopics.js.map +1 -1
  65. package/dist/tools/developTextTool.d.ts +2 -0
  66. package/dist/tools/developTextTool.d.ts.map +1 -1
  67. package/dist/tools/developTextTool.js +1 -0
  68. package/dist/tools/developTextTool.js.map +1 -1
  69. package/dist/tools/enableMemory.d.ts +29 -0
  70. package/dist/tools/enableMemory.d.ts.map +1 -0
  71. package/dist/tools/enableMemory.js +35 -0
  72. package/dist/tools/enableMemory.js.map +1 -0
  73. package/dist/tools/enableProjectLearnings.d.ts +1 -0
  74. package/dist/tools/enableProjectLearnings.d.ts.map +1 -1
  75. package/dist/tools/enableProjectLearnings.js +7 -40
  76. package/dist/tools/enableProjectLearnings.js.map +1 -1
  77. package/dist/tools/generateContentGaps.d.ts +2 -0
  78. package/dist/tools/generateContentGaps.d.ts.map +1 -1
  79. package/dist/tools/generateContentGaps.js +1 -0
  80. package/dist/tools/generateContentGaps.js.map +1 -1
  81. package/dist/tools/generateContextualHint.d.ts +2 -0
  82. package/dist/tools/generateContextualHint.d.ts.map +1 -1
  83. package/dist/tools/generateContextualHint.js +1 -0
  84. package/dist/tools/generateContextualHint.js.map +1 -1
  85. package/dist/tools/generateDifferenceGraphFromTexts.d.ts +3 -0
  86. package/dist/tools/generateDifferenceGraphFromTexts.d.ts.map +1 -1
  87. package/dist/tools/generateDifferenceGraphFromTexts.js +3 -1
  88. package/dist/tools/generateDifferenceGraphFromTexts.js.map +1 -1
  89. package/dist/tools/generateGoogleResultsVsQueriesGraph.d.ts +1 -0
  90. package/dist/tools/generateGoogleResultsVsQueriesGraph.d.ts.map +1 -1
  91. package/dist/tools/generateGoogleResultsVsQueriesGraph.js +1 -0
  92. package/dist/tools/generateGoogleResultsVsQueriesGraph.js.map +1 -1
  93. package/dist/tools/generateGoogleSearchQueriesGraph.d.ts +1 -0
  94. package/dist/tools/generateGoogleSearchQueriesGraph.d.ts.map +1 -1
  95. package/dist/tools/generateGoogleSearchQueriesGraph.js +1 -0
  96. package/dist/tools/generateGoogleSearchQueriesGraph.js.map +1 -1
  97. package/dist/tools/generateGoogleSearchResultsGraph.d.ts +1 -0
  98. package/dist/tools/generateGoogleSearchResultsGraph.d.ts.map +1 -1
  99. package/dist/tools/generateGoogleSearchResultsGraph.js +1 -0
  100. package/dist/tools/generateGoogleSearchResultsGraph.js.map +1 -1
  101. package/dist/tools/generateKnowledgeGraph.d.ts +3 -0
  102. package/dist/tools/generateKnowledgeGraph.d.ts.map +1 -1
  103. package/dist/tools/generateKnowledgeGraph.js +3 -1
  104. package/dist/tools/generateKnowledgeGraph.js.map +1 -1
  105. package/dist/tools/generateMergedGraphFromTexts.d.ts +3 -0
  106. package/dist/tools/generateMergedGraphFromTexts.d.ts.map +1 -1
  107. package/dist/tools/generateMergedGraphFromTexts.js +3 -1
  108. package/dist/tools/generateMergedGraphFromTexts.js.map +1 -1
  109. package/dist/tools/generateOntologyGraph.d.ts +2 -0
  110. package/dist/tools/generateOntologyGraph.d.ts.map +1 -1
  111. package/dist/tools/generateOntologyGraph.js +3 -1
  112. package/dist/tools/generateOntologyGraph.js.map +1 -1
  113. package/dist/tools/generateOverlapGraphFromTexts.d.ts +3 -0
  114. package/dist/tools/generateOverlapGraphFromTexts.d.ts.map +1 -1
  115. package/dist/tools/generateOverlapGraphFromTexts.js +3 -1
  116. package/dist/tools/generateOverlapGraphFromTexts.js.map +1 -1
  117. package/dist/tools/generateResearchIdeas.d.ts +2 -0
  118. package/dist/tools/generateResearchIdeas.d.ts.map +1 -1
  119. package/dist/tools/generateResearchIdeas.js +1 -0
  120. package/dist/tools/generateResearchIdeas.js.map +1 -1
  121. package/dist/tools/generateResearchQuestions.d.ts +2 -0
  122. package/dist/tools/generateResearchQuestions.d.ts.map +1 -1
  123. package/dist/tools/generateResearchQuestions.js +1 -0
  124. package/dist/tools/generateResearchQuestions.js.map +1 -1
  125. package/dist/tools/generateResponses.d.ts +2 -0
  126. package/dist/tools/generateResponses.d.ts.map +1 -1
  127. package/dist/tools/generateResponses.js +1 -0
  128. package/dist/tools/generateResponses.js.map +1 -1
  129. package/dist/tools/generateSEOGraphTool.d.ts +2 -0
  130. package/dist/tools/generateSEOGraphTool.d.ts.map +1 -1
  131. package/dist/tools/generateSEOGraphTool.js +2 -1
  132. package/dist/tools/generateSEOGraphTool.js.map +1 -1
  133. package/dist/tools/generateTopicalClusters.d.ts +2 -0
  134. package/dist/tools/generateTopicalClusters.d.ts.map +1 -1
  135. package/dist/tools/generateTopicalClusters.js +1 -0
  136. package/dist/tools/generateTopicalClusters.js.map +1 -1
  137. package/dist/tools/generateYoutubeSearchResultsGraph.d.ts +1 -0
  138. package/dist/tools/generateYoutubeSearchResultsGraph.d.ts.map +1 -1
  139. package/dist/tools/generateYoutubeSearchResultsGraph.js +1 -0
  140. package/dist/tools/generateYoutubeSearchResultsGraph.js.map +1 -1
  141. package/dist/tools/getMemory.d.ts +8 -7
  142. package/dist/tools/getMemory.d.ts.map +1 -1
  143. package/dist/tools/getMemory.js +19 -2
  144. package/dist/tools/getMemory.js.map +1 -1
  145. package/dist/tools/getNodeNeighborhood.d.ts +30 -0
  146. package/dist/tools/getNodeNeighborhood.d.ts.map +1 -0
  147. package/dist/tools/getNodeNeighborhood.js +68 -0
  148. package/dist/tools/getNodeNeighborhood.js.map +1 -0
  149. package/dist/tools/getProjectLearnings.d.ts +2 -0
  150. package/dist/tools/getProjectLearnings.d.ts.map +1 -1
  151. package/dist/tools/getProjectLearnings.js +12 -125
  152. package/dist/tools/getProjectLearnings.js.map +1 -1
  153. package/dist/tools/index.d.ts +3 -0
  154. package/dist/tools/index.d.ts.map +1 -1
  155. package/dist/tools/index.js +3 -0
  156. package/dist/tools/index.js.map +1 -1
  157. package/dist/tools/listGraphs.d.ts +1 -0
  158. package/dist/tools/listGraphs.d.ts.map +1 -1
  159. package/dist/tools/listGraphs.js +1 -0
  160. package/dist/tools/listGraphs.js.map +1 -1
  161. package/dist/tools/optimizeKnowledgeBase.d.ts +1 -0
  162. package/dist/tools/optimizeKnowledgeBase.d.ts.map +1 -1
  163. package/dist/tools/optimizeKnowledgeBase.js +19 -6
  164. package/dist/tools/optimizeKnowledgeBase.js.map +1 -1
  165. package/dist/tools/optimizeReasoning.d.ts +2 -0
  166. package/dist/tools/optimizeReasoning.d.ts.map +1 -1
  167. package/dist/tools/optimizeReasoning.js +1 -0
  168. package/dist/tools/optimizeReasoning.js.map +1 -1
  169. package/dist/tools/optimizeTextStructure.d.ts +2 -0
  170. package/dist/tools/optimizeTextStructure.d.ts.map +1 -1
  171. package/dist/tools/optimizeTextStructure.js +1 -0
  172. package/dist/tools/optimizeTextStructure.js.map +1 -1
  173. package/dist/tools/retrieveContextForPromptFromGraph.d.ts +3 -0
  174. package/dist/tools/retrieveContextForPromptFromGraph.d.ts.map +1 -1
  175. package/dist/tools/retrieveContextForPromptFromGraph.js +4 -1
  176. package/dist/tools/retrieveContextForPromptFromGraph.js.map +1 -1
  177. package/dist/tools/searchGraphs.d.ts +1 -0
  178. package/dist/tools/searchGraphs.d.ts.map +1 -1
  179. package/dist/tools/searchGraphs.js +1 -0
  180. package/dist/tools/searchGraphs.js.map +1 -1
  181. package/dist/tools/searchGraphsFetch.d.ts +1 -0
  182. package/dist/tools/searchGraphsFetch.d.ts.map +1 -1
  183. package/dist/tools/searchGraphsFetch.js +2 -1
  184. package/dist/tools/searchGraphsFetch.js.map +1 -1
  185. package/dist/tools/updateStatements.d.ts +2 -2
  186. package/dist/types/index.d.ts +97 -0
  187. package/dist/types/index.d.ts.map +1 -1
  188. package/dist/utils/elicitApproval.d.ts +42 -0
  189. package/dist/utils/elicitApproval.d.ts.map +1 -0
  190. package/dist/utils/elicitApproval.js +83 -0
  191. package/dist/utils/elicitApproval.js.map +1 -0
  192. package/dist/utils/graphInput.d.ts +5 -1
  193. package/dist/utils/graphInput.d.ts.map +1 -1
  194. package/dist/utils/graphInput.js +4 -3
  195. package/dist/utils/graphInput.js.map +1 -1
  196. package/dist/utils/learnings.d.ts +4 -2
  197. package/dist/utils/learnings.d.ts.map +1 -1
  198. package/dist/utils/learnings.js +4 -2
  199. package/dist/utils/learnings.js.map +1 -1
  200. package/dist/utils/memoryResult.d.ts +23 -0
  201. package/dist/utils/memoryResult.d.ts.map +1 -0
  202. package/dist/utils/memoryResult.js +27 -0
  203. package/dist/utils/memoryResult.js.map +1 -0
  204. package/dist/utils/textSignature.d.ts +231 -0
  205. package/dist/utils/textSignature.d.ts.map +1 -0
  206. package/dist/utils/textSignature.js +738 -0
  207. package/dist/utils/textSignature.js.map +1 -0
  208. package/dist/utils/transformers.d.ts +3 -2
  209. package/dist/utils/transformers.d.ts.map +1 -1
  210. package/dist/utils/transformers.js +29 -1
  211. package/dist/utils/transformers.js.map +1 -1
  212. package/dist/utils/wikilinksMode.d.ts +12 -2
  213. package/dist/utils/wikilinksMode.d.ts.map +1 -1
  214. package/dist/utils/wikilinksMode.js +18 -5
  215. package/dist/utils/wikilinksMode.js.map +1 -1
  216. package/llms-full.infranodus.txt +30 -9
  217. package/llms-full.txt +30 -9
  218. package/llms.infranodus.txt +6 -2
  219. package/llms.txt +6 -2
  220. package/package.json +4 -2
@@ -0,0 +1,738 @@
1
+ import { trimDegreeDistribution } from "./transformers.js";
2
+ // ---------------------------------------------------------------------------
3
+ // Basic statistics
4
+ // ---------------------------------------------------------------------------
5
+ const round = (value, digits = 3) => value == null || !Number.isFinite(value) ? null : Number(value.toFixed(digits));
6
+ const mean = (values) => values.length ? values.reduce((a, b) => a + b, 0) / values.length : NaN;
7
+ const std = (values) => {
8
+ if (values.length < 2)
9
+ return NaN;
10
+ const m = mean(values);
11
+ return Math.sqrt(mean(values.map((v) => (v - m) * (v - m))));
12
+ };
13
+ const quantile = (values, q) => {
14
+ if (!values.length)
15
+ return NaN;
16
+ const sorted = [...values].sort((a, b) => a - b);
17
+ const pos = (sorted.length - 1) * q;
18
+ const lo = Math.floor(pos);
19
+ const hi = Math.ceil(pos);
20
+ return sorted[lo] + (sorted[hi] - sorted[lo]) * (pos - lo);
21
+ };
22
+ /** Lag-1 autocorrelation; null when the series is constant or too short. */
23
+ const lag1Autocorrelation = (values) => {
24
+ if (values.length < 8)
25
+ return null;
26
+ const m = mean(values);
27
+ let num = 0;
28
+ let den = 0;
29
+ for (let i = 0; i < values.length; i++) {
30
+ den += (values[i] - m) ** 2;
31
+ if (i + 1 < values.length)
32
+ num += (values[i] - m) * (values[i + 1] - m);
33
+ }
34
+ return den > 0 ? num / den : null;
35
+ };
36
+ const isFinitePoint = (p) => p != null &&
37
+ typeof p === "object" &&
38
+ Number.isFinite(p.x) &&
39
+ Number.isFinite(p.y);
40
+ const toPoint = (p) => ({
41
+ x: p.x,
42
+ y: p.y,
43
+ z: Number.isFinite(p.z) ? p.z : 0,
44
+ });
45
+ const distance = (a, b) => Math.sqrt((a.x - b.x) ** 2 + (a.y - b.y) ** 2 + (a.z - b.z) ** 2);
46
+ const centroid = (points) => ({
47
+ x: mean(points.map((p) => p.x)),
48
+ y: mean(points.map((p) => p.y)),
49
+ z: mean(points.map((p) => p.z)),
50
+ });
51
+ // Hashtags occasionally arrive with a leading "#"; node keys never do.
52
+ const stripHash = (key) => (key.startsWith("#") ? key.slice(1) : key);
53
+ /** Root-mean-square distance of the nodes from their centroid: the scale unit. */
54
+ export function graphRadius(nodes) {
55
+ const points = nodes
56
+ .map((n) => n.attributes)
57
+ .filter(isFinitePoint)
58
+ .map(toPoint);
59
+ if (points.length < 2)
60
+ return null;
61
+ const c = centroid(points);
62
+ const radius = Math.sqrt(mean(points.map((p) => distance(p, c) ** 2)));
63
+ return radius > 0 ? radius : null;
64
+ }
65
+ function amplitudeOfPath(path, radius) {
66
+ if (path.length < 2 || !radius)
67
+ return null;
68
+ const points = path.map((p) => p.point);
69
+ const steps = [];
70
+ for (let i = 0; i + 1 < points.length; i++)
71
+ steps.push(distance(points[i], points[i + 1]) / radius);
72
+ const c = centroid(points);
73
+ const radial = points.map((p) => distance(p, c) / radius);
74
+ // Cluster crossings: a step whose endpoints belong to different clusters.
75
+ let crossings = [];
76
+ let known = 0;
77
+ for (let i = 0; i + 1 < path.length; i++) {
78
+ const a = path[i].community;
79
+ const b = path[i + 1].community;
80
+ if (a == null || b == null)
81
+ continue;
82
+ known++;
83
+ if (a !== b)
84
+ crossings.push(i);
85
+ }
86
+ if (known < steps.length / 2)
87
+ crossings = null;
88
+ const gaps = [];
89
+ if (crossings)
90
+ for (let i = 1; i < crossings.length; i++)
91
+ gaps.push(crossings[i] - crossings[i - 1]);
92
+ const stepMean = mean(steps);
93
+ const radialMean = mean(radial);
94
+ return {
95
+ n: steps.length,
96
+ meanStep: round(stepMean),
97
+ medianStep: round(quantile(steps, 0.5)),
98
+ p95Step: round(quantile(steps, 0.95)),
99
+ cvStep: stepMean > 0 ? round(std(steps) / stepMean) : null,
100
+ lag1Autocorrelation: round(lag1Autocorrelation(steps)),
101
+ crossClusterShare: crossings ? round(crossings.length / known) : null,
102
+ crossClusterRunCV: gaps.length >= 5 && mean(gaps) > 0 ? round(std(gaps) / mean(gaps)) : null,
103
+ meanRadial: round(radialMean),
104
+ cvRadial: radialMean > 0 ? round(std(radial) / radialMean) : null,
105
+ };
106
+ }
107
+ /**
108
+ * Rebuild the statement and word paths the backend ran DFA on (statements at
109
+ * graphCoordinates, words at their node positions in statementHashtags order)
110
+ * and measure their amplitude.
111
+ */
112
+ export function computeAmplitude(nodes, statements) {
113
+ const radius = graphRadius(nodes);
114
+ const nodeByKey = new Map();
115
+ for (const node of nodes)
116
+ nodeByKey.set(node.key, node);
117
+ const ordered = [...statements].sort((a, b) => (a.sortId ?? 0) - (b.sortId ?? 0));
118
+ const statementPath = [];
119
+ const wordPath = [];
120
+ for (const statement of ordered) {
121
+ if (isFinitePoint(statement.graphCoordinates)) {
122
+ statementPath.push({
123
+ point: toPoint(statement.graphCoordinates),
124
+ community: statement.topStatementCommunity != null ? String(statement.topStatementCommunity) : null,
125
+ });
126
+ }
127
+ for (const tag of statement.statementHashtags ?? []) {
128
+ const attributes = nodeByKey.get(stripHash(tag))?.attributes;
129
+ const community = attributes?.community;
130
+ if (!attributes || !isFinitePoint(attributes))
131
+ continue;
132
+ wordPath.push({
133
+ point: toPoint(attributes),
134
+ community: community != null ? String(community) : null,
135
+ });
136
+ }
137
+ }
138
+ return {
139
+ graphRadius: round(radius),
140
+ statements: amplitudeOfPath(statementPath, radius),
141
+ words: amplitudeOfPath(wordPath, radius),
142
+ };
143
+ }
144
+ const SENTENCE_SPLIT = /(?<=[.!?…])\s+(?=[^\s])/;
145
+ const ABBREVIATIONS = /\b(?:e\.g|i\.e|etc|vs|dr|mr|mrs|ms|prof|st|no|fig|cf)\.$/i;
146
+ /** Split one statement into sentences; joins fragments cut at a known abbreviation. */
147
+ export function splitSentences(text) {
148
+ const pieces = text.split(SENTENCE_SPLIT);
149
+ const out = [];
150
+ for (const piece of pieces) {
151
+ const trimmed = piece.trim();
152
+ if (!trimmed)
153
+ continue;
154
+ if (out.length && ABBREVIATIONS.test(out[out.length - 1]))
155
+ out[out.length - 1] += " " + trimmed;
156
+ else
157
+ out.push(trimmed);
158
+ }
159
+ return out;
160
+ }
161
+ const wordCount = (text) => text.split(/\s+/).filter((w) => /[\p{L}\p{N}]/u.test(w)).length;
162
+ // Statements shorter than this are treated as fragments (headings, menu
163
+ // items, list bullets, captions) and left out of the sentence rhythm: on a
164
+ // fetched web page they outnumber the prose and flatten every text to the
165
+ // same profile.
166
+ export const PROSE_MIN_WORDS = 12;
167
+ export function computeSentenceRhythm(statements) {
168
+ const ordered = [...statements].sort((a, b) => (a.sortId ?? 0) - (b.sortId ?? 0));
169
+ const lengths = [];
170
+ const statementLengths = [];
171
+ let skipped = 0;
172
+ for (const statement of ordered) {
173
+ const content = statement.content?.trim();
174
+ if (!content)
175
+ continue;
176
+ const total = wordCount(content);
177
+ if (total < PROSE_MIN_WORDS) {
178
+ skipped++;
179
+ continue;
180
+ }
181
+ statementLengths.push(total);
182
+ for (const sentence of splitSentences(content)) {
183
+ const n = wordCount(sentence);
184
+ if (n > 0)
185
+ lengths.push(n);
186
+ }
187
+ }
188
+ const m = mean(lengths);
189
+ const sm = mean(statementLengths);
190
+ return {
191
+ sentences: lengths.length,
192
+ skippedFragments: skipped,
193
+ meanLength: round(m, 1),
194
+ cvLength: lengths.length >= 2 && m > 0 ? round(std(lengths) / m) : null,
195
+ lag1Autocorrelation: round(lag1Autocorrelation(lengths)),
196
+ shortShare: lengths.length ? round(lengths.filter((n) => n < 8).length / lengths.length) : null,
197
+ longShare: lengths.length ? round(lengths.filter((n) => n > 30).length / lengths.length) : null,
198
+ statements: {
199
+ n: statementLengths.length,
200
+ meanLength: round(sm, 1),
201
+ cvLength: statementLengths.length >= 2 && sm > 0 ? round(std(statementLengths) / sm) : null,
202
+ },
203
+ };
204
+ }
205
+ /** The text-specific reading bands of a DFA exponent on the step-length series. */
206
+ export function rhythmBand(alpha) {
207
+ if (alpha == null || !Number.isFinite(alpha))
208
+ return null;
209
+ if (alpha < 0.45)
210
+ return "alternating";
211
+ if (alpha < 0.6)
212
+ return "random";
213
+ if (alpha <= 0.9)
214
+ return "fractal";
215
+ if (alpha < 1.0)
216
+ return "persistent";
217
+ return "blocks";
218
+ }
219
+ const STEP_READINGS = {
220
+ alternating: "The text keeps switching back and forth between two or more topics, like a dialogue or a comparison.",
221
+ random: "Each idea has little to do with the one before it: a list, a thread, or loose notes.",
222
+ fractal: "The text moves between topics but keeps coming back to them, the way an essay or a story does.",
223
+ persistent: "The text stays with each topic for a long time before moving on, but still returns to earlier ones.",
224
+ blocks: "The text works through one topic at a time and rarely goes back, like chapters or sections.",
225
+ };
226
+ export function readStepAlpha(alpha) {
227
+ const band = rhythmBand(alpha);
228
+ return band ? STEP_READINGS[band] : null;
229
+ }
230
+ export function readRadialAlpha(alpha) {
231
+ const band = rhythmBand(alpha);
232
+ if (!band)
233
+ return null;
234
+ switch (band) {
235
+ case "alternating":
236
+ return "The text swings in and out of its main theme in quick alternation.";
237
+ case "random":
238
+ return "The text stays at a roughly constant distance from its main theme, with no pattern to its excursions.";
239
+ case "fractal":
240
+ case "persistent":
241
+ return "The text leaves its main theme and comes back to it, repeatedly.";
242
+ case "blocks":
243
+ return "The text drifts away from where it started and does not return.";
244
+ }
245
+ }
246
+ export function readScales(series) {
247
+ if (!series || series.alpha1 == null || series.alpha2 == null)
248
+ return null;
249
+ const short = rhythmBand(series.alpha1);
250
+ const long = rhythmBand(series.alpha2);
251
+ if (!short || !long)
252
+ return null;
253
+ const shortCoherent = short === "fractal" || short === "persistent" || short === "blocks";
254
+ const longCoherent = long === "fractal" || long === "persistent" || long === "blocks";
255
+ if (shortCoherent && !longCoherent)
256
+ return "Paragraphs hold together, but the document as a whole does not follow a thread.";
257
+ if (!shortCoherent && longCoherent)
258
+ return "Sentences jump around, but the overall arc is planned.";
259
+ if (shortCoherent && longCoherent)
260
+ return "Both the local flow and the overall arc hold together.";
261
+ return "Neither the local flow nor the overall arc follows a thread.";
262
+ }
263
+ export function readMultifractal(series) {
264
+ const mf = series?.multifractal;
265
+ if (!mf)
266
+ return null;
267
+ if (mf.significant === false)
268
+ return "The pacing shows no multifractal structure beyond what its length would produce by chance.";
269
+ switch (mf.label) {
270
+ case "multifractal":
271
+ return mf.source === "distribution"
272
+ ? "Some leaps are very large, but they are not placed in any pattern."
273
+ : "Long calm stretches broken by rare big leaps.";
274
+ case "weak":
275
+ return "The pacing is slightly uneven: a few leaps stand out from the rest.";
276
+ case "monofractal":
277
+ return "The pacing is steady throughout.";
278
+ default:
279
+ return null;
280
+ }
281
+ }
282
+ /** From the backend's minimum-length table. */
283
+ export function confidenceFor(n) {
284
+ if (n == null || n < 64)
285
+ return "none";
286
+ if (n < 128)
287
+ return "indicative";
288
+ if (n < 256)
289
+ return "usable";
290
+ return "reliable";
291
+ }
292
+ export function readLevel(level) {
293
+ if (!level || (!level.byStepLength && !level.byRadialDistance))
294
+ return null;
295
+ const step = level.byStepLength;
296
+ return {
297
+ stepLength: readStepAlpha(step?.alphaBounded),
298
+ radialDistance: readRadialAlpha(level.byRadialDistance?.alphaBounded),
299
+ scales: readScales(step),
300
+ multifractal: readMultifractal(step),
301
+ confidence: confidenceFor(step?.n ?? level.byRadialDistance?.n),
302
+ };
303
+ }
304
+ // The sentence-length level is not a path: it says nothing about topics,
305
+ // only about rhythm, so it gets its own wording.
306
+ const SENTENCE_ALPHA_READINGS = {
307
+ alternating: "Long and short sentences alternate in a regular beat.",
308
+ random: "Each sentence's length has nothing to do with the one before: no rhythm across sentences.",
309
+ fractal: "Runs of long and short sentences nest at every scale, the 1/f rhythm of literary prose.",
310
+ persistent: "Long stretches of similar sentence length, with some return to the earlier pace.",
311
+ blocks: "The pace drifts: whole sections run long or short and do not come back.",
312
+ };
313
+ export function readSentenceAlpha(alpha) {
314
+ const band = rhythmBand(alpha);
315
+ return band ? SENTENCE_ALPHA_READINGS[band] : null;
316
+ }
317
+ /** Summary of the backend's DFA on the sentence-length series (fractal_variability.sentenceLength.byWords). */
318
+ export function summariseSentenceScaling(fractal) {
319
+ const series = fractal?.sentenceLength?.byWords;
320
+ if (!series)
321
+ return null;
322
+ const short = rhythmBand(series.alpha1);
323
+ const long = rhythmBand(series.alpha2);
324
+ let scales = null;
325
+ if (short && long && short !== long) {
326
+ const coherent = (b) => b === "fractal" || b === "persistent" || b === "blocks";
327
+ if (coherent(short) && !coherent(long))
328
+ scales = "Neighbouring sentences follow a rhythm, but it does not carry across paragraphs.";
329
+ else if (!coherent(short) && coherent(long))
330
+ scales = "Sentence to sentence the pace is unpatterned, but sections as a whole run long or short.";
331
+ }
332
+ return {
333
+ n: series.n,
334
+ alphaBounded: series.alphaBounded,
335
+ alphaLabel: series.alphaLabel,
336
+ alpha1: series.alpha1 ?? null,
337
+ alpha2: series.alpha2 ?? null,
338
+ multifractalLabel: series.multifractal?.label ?? null,
339
+ readings: {
340
+ alpha: readSentenceAlpha(series.alphaBounded),
341
+ scales,
342
+ multifractal: readMultifractal(series),
343
+ confidence: confidenceFor(series.n),
344
+ },
345
+ };
346
+ }
347
+ // The influence level: how the text moves between its hub concepts and its
348
+ // periphery. Read on the betweenness-rank series only.
349
+ const INFLUENCE_READINGS = {
350
+ alternating: "The text swings between its central concepts and peripheral ones in a regular beat.",
351
+ random: "Central and peripheral concepts follow each other without pattern.",
352
+ fractal: "The text returns to its central concepts in nested cycles, at every scale.",
353
+ persistent: "Long stretches near the central concepts, or away from them, with some return.",
354
+ blocks: "The text stays with its central concepts for whole sections, then leaves them, or the reverse, without return.",
355
+ };
356
+ export function readInfluenceAlpha(alpha) {
357
+ const band = rhythmBand(alpha);
358
+ return band ? INFLUENCE_READINGS[band] : null;
359
+ }
360
+ export function readInfluence(fractal) {
361
+ const series = fractal?.influence?.byBetweennessRank;
362
+ if (!series)
363
+ return null;
364
+ return {
365
+ byBetweennessRank: readInfluenceAlpha(series.alphaBounded),
366
+ scales: readScales(series),
367
+ multifractal: readMultifractal(series),
368
+ confidence: confidenceFor(series.n),
369
+ };
370
+ }
371
+ /** Concentration of connectivity in the whole network; a descriptor, never a scale-free verdict. */
372
+ export function readDegreeDistribution(distribution) {
373
+ if (!distribution)
374
+ return null;
375
+ const out = {};
376
+ if (typeof distribution.gini === "number") {
377
+ out.gini =
378
+ distribution.gini < 0.3
379
+ ? "Connections are spread evenly across the concepts."
380
+ : distribution.gini <= 0.5
381
+ ? "Connections are moderately concentrated: some concepts are far better connected than most."
382
+ : "A few hub concepts hold most of the connections.";
383
+ }
384
+ out.tail = distribution.tail
385
+ ? `The top ${distribution.tail.n} concepts (degree ${distribution.tail.xmin} and above) thin out with a tail exponent of ${distribution.tail.alpha.toFixed(2)}: lower is heavier. This describes concentration only; it is not evidence of a scale-free network.`
386
+ : "Too few concepts (under 50) to describe the tail.";
387
+ if (typeof distribution.nodes === "number") {
388
+ out.nodes = `The whole co-occurrence network has ${distribution.nodes} concepts before the node cap.`;
389
+ }
390
+ return out;
391
+ }
392
+ export function readAmplitude(stats) {
393
+ if (!stats)
394
+ return null;
395
+ const out = {};
396
+ if (stats.meanStep != null) {
397
+ out.meanStep =
398
+ stats.meanStep < 0.5
399
+ ? "Consecutive ideas sit close together in the concept network."
400
+ : stats.meanStep < 1.0
401
+ ? "Consecutive ideas are a moderate distance apart in the concept network."
402
+ : "Consecutive ideas are typically far apart in the concept network.";
403
+ }
404
+ if (stats.cvStep != null) {
405
+ out.cvStep =
406
+ stats.cvStep < 0.4
407
+ ? "Every move between ideas is about the same size; the pacing is even."
408
+ : stats.cvStep <= 0.8
409
+ ? "The moves between ideas vary in size; the pacing is varied."
410
+ : "Mostly small moves with a few big leaps; the pacing is uneven.";
411
+ }
412
+ if (stats.lag1Autocorrelation != null) {
413
+ out.lag1Autocorrelation =
414
+ stats.lag1Autocorrelation > 0.15
415
+ ? "Big moves tend to follow big moves: the text has stretches of leaping and stretches of staying."
416
+ : stats.lag1Autocorrelation < -0.15
417
+ ? "Big moves tend to be followed by small ones: the text leaps and then settles."
418
+ : "The size of one move says little about the next.";
419
+ }
420
+ if (stats.crossClusterShare != null) {
421
+ const share = stats.crossClusterShare;
422
+ out.crossClusterShare =
423
+ share === 0
424
+ ? "The text never crosses into a different topic."
425
+ : share > 0.6
426
+ ? `Most moves (${Math.round(share * 100)}%) cross into a different topic.`
427
+ : `About one move in ${Math.round(1 / share)} crosses into a different topic.`;
428
+ }
429
+ if (stats.crossClusterRunCV != null) {
430
+ out.crossClusterRunCV =
431
+ stats.crossClusterRunCV < 0.7
432
+ ? "Topic changes come at regular intervals, like items in an outline."
433
+ : stats.crossClusterRunCV <= 1.3
434
+ ? "Topic changes come at irregular but unpatterned intervals."
435
+ : "Topic changes come in bursts: several in a row, then a long stay.";
436
+ }
437
+ if (stats.cvRadial != null) {
438
+ out.cvRadial =
439
+ stats.cvRadial < 0.3
440
+ ? "The text keeps a steady distance from its core theme."
441
+ : "The text alternates between its core theme and its edges.";
442
+ }
443
+ return out;
444
+ }
445
+ export function readSentenceRhythm(rhythm) {
446
+ const out = {};
447
+ if (rhythm.cvLength != null) {
448
+ out.cvLength =
449
+ rhythm.cvLength < 0.35
450
+ ? "Sentences are all about the same length; the rhythm is flat."
451
+ : rhythm.cvLength < 0.55
452
+ ? "Sentence length varies moderately."
453
+ : "Sentence length varies a lot: long sentences next to short ones.";
454
+ }
455
+ if (rhythm.shortShare != null) {
456
+ out.shortShare =
457
+ rhythm.shortShare < 0.05
458
+ ? "Almost no short sentences."
459
+ : rhythm.shortShare > 0.2
460
+ ? "Many short sentences."
461
+ : "Some short sentences.";
462
+ }
463
+ if (rhythm.lag1Autocorrelation != null) {
464
+ out.lag1Autocorrelation =
465
+ rhythm.lag1Autocorrelation > 0.15
466
+ ? "Long sentences cluster together, and so do short ones."
467
+ : rhythm.lag1Autocorrelation < -0.15
468
+ ? "Long and short sentences alternate."
469
+ : "Sentence lengths do not follow one another in any pattern.";
470
+ }
471
+ return out;
472
+ }
473
+ const DIVERSITY_READINGS = {
474
+ biased: "One topic dominates; the discourse has few places to move between.",
475
+ focused: "A main topic with some satellites; developed but narrow.",
476
+ diversified: "Several distinct topics with comparable weight; the discourse has room to move.",
477
+ dispersed: "Many weakly connected fragments; the text is scattered rather than structured.",
478
+ };
479
+ export function readStructure(stats) {
480
+ if (!stats)
481
+ return null;
482
+ const out = {};
483
+ const score = stats.diversity_score?.toLowerCase();
484
+ out.diversity = (score && DIVERSITY_READINGS[score]) || null;
485
+ if (stats.top_nodes_entropy != null) {
486
+ out.topNodesEntropy =
487
+ stats.top_nodes_entropy >= 1.5
488
+ ? "Influence is spread across many concepts."
489
+ : stats.top_nodes_entropy >= 0.8
490
+ ? "A few concepts carry most of the influence."
491
+ : "The text hinges on one or two hub concepts.";
492
+ }
493
+ if (stats.ratio_of_top_cluster_influence_by_betweenness != null && stats.fair_influence_by_cluster) {
494
+ const times = stats.ratio_of_top_cluster_influence_by_betweenness / stats.fair_influence_by_cluster;
495
+ out.clusterDominance =
496
+ times >= 2.5
497
+ ? `The largest topic carries about ${times.toFixed(1)}x its fair share of the discourse.`
498
+ : times >= 1.5
499
+ ? `The largest topic carries about ${times.toFixed(1)}x its fair share of the discourse; the rest still have weight.`
500
+ : "The topics share the discourse fairly evenly.";
501
+ }
502
+ return out;
503
+ }
504
+ const SIGNATURE_GRID = {
505
+ biased: { alternating: "back-and-forth", random: "boilerplate", fractal: "deep dive", blocks: "monologue" },
506
+ focused: { alternating: "comparison", random: "report", fractal: "essay", blocks: "treatise" },
507
+ diversified: { alternating: "dialogue", random: "digest", fractal: "narrative", blocks: "anthology" },
508
+ dispersed: { alternating: "ping-pong", random: "notes", fractal: "stream of consciousness", blocks: "drift" },
509
+ };
510
+ const SIGNATURE_SUMMARIES = {
511
+ "back-and-forth": "one topic, argued from alternating sides",
512
+ boilerplate: "one topic, listed rather than developed",
513
+ "deep dive": "one theme, developed in layers that keep returning to it",
514
+ monologue: "one theme, pursued in a straight line without return",
515
+ comparison: "a main topic examined by alternating between its facets",
516
+ report: "a main topic with its points listed one after another",
517
+ essay: "an argument that circles its core and returns to it at every scale",
518
+ treatise: "a main topic worked through in sections, each closed before the next",
519
+ dialogue: "several topics taken in turns, as in a conversation",
520
+ digest: "several topics with no path between them: a thread or a compilation",
521
+ narrative: "several topics woven together and revisited at every scale",
522
+ anthology: "several topics treated one after another in separate blocks",
523
+ "ping-pong": "scattered fragments that alternate without settling",
524
+ notes: "scattered fragments with no thread: a scratchpad",
525
+ "stream of consciousness": "scattered fragments that nonetheless return to each other",
526
+ drift: "scattered fragments that wander off and do not come back",
527
+ };
528
+ /** The headline series: statements when long enough, else words (never ngrams). */
529
+ export function headlineSeries(fractal) {
530
+ if (fractal?.statements?.byStepLength && confidenceFor(fractal.statements.byStepLength.n) !== "indicative")
531
+ return { level: "statements", series: fractal.statements.byStepLength };
532
+ if (fractal?.words?.byStepLength)
533
+ return { level: "words", series: fractal.words.byStepLength };
534
+ if (fractal?.statements?.byStepLength)
535
+ return { level: "statements", series: fractal.statements.byStepLength };
536
+ return { level: null, series: null };
537
+ }
538
+ export function composeSignature(diversity, fractal) {
539
+ const score = diversity?.diversity_score?.toLowerCase();
540
+ const structure = (score && score in SIGNATURE_GRID ? score : null);
541
+ const { level, series } = headlineSeries(fractal);
542
+ const band = rhythmBand(series?.alphaBounded);
543
+ const rhythm = band === "persistent" ? "fractal" : band;
544
+ const label = structure && rhythm ? SIGNATURE_GRID[structure][rhythm] : null;
545
+ return {
546
+ label,
547
+ structure,
548
+ rhythm,
549
+ rhythmSource: level,
550
+ summary: label ? SIGNATURE_SUMMARIES[label] : null,
551
+ };
552
+ }
553
+ const CAVEATS = [
554
+ "This is a stylistic tendency read from rhythm and structure, not a detection result; never treat it as proof of authorship.",
555
+ "The thresholds are heuristics from the DFA and stylometry literature. On a 50-page check against a commercial detector they caught only uniformly paced template text; AI-drafted pages that were edited or humanized read as human here.",
556
+ "Even pacing is also typical of human technical, legal, and non-native writing; uneven pacing can be prompted or paraphrased into generated text.",
557
+ "Under about 30 sentences or 128 path steps the rhythm measures are noisy and the verdict should be read as inconclusive.",
558
+ ];
559
+ export function estimateAiLikeness(input) {
560
+ const evidence = [];
561
+ const { sentenceRhythm, amplitude, fractal } = input;
562
+ // Classic burstiness: the strongest cheap signal.
563
+ if (sentenceRhythm.cvLength != null && sentenceRhythm.sentences >= 10) {
564
+ const cv = sentenceRhythm.cvLength;
565
+ const direction = cv < 0.35 ? 1 : cv < 0.5 ? 0.3 : cv < 0.7 ? -0.5 : -1;
566
+ evidence.push({
567
+ signal: "sentence length variability (CV)",
568
+ value: cv,
569
+ direction,
570
+ weight: 2,
571
+ reading: direction > 0
572
+ ? "Uniform sentence lengths, the flat rhythm typical of generated text."
573
+ : "Uneven sentence lengths, the bursty rhythm typical of human prose.",
574
+ });
575
+ }
576
+ if (sentenceRhythm.shortShare != null && sentenceRhythm.sentences >= 20) {
577
+ const share = sentenceRhythm.shortShare;
578
+ const direction = share < 0.05 ? 0.5 : share > 0.15 ? -0.5 : 0;
579
+ if (direction !== 0)
580
+ evidence.push({
581
+ signal: "share of short sentences",
582
+ value: share,
583
+ direction,
584
+ weight: 0.5,
585
+ reading: direction > 0 ? "Almost no short sentences." : "Short sentences are used freely.",
586
+ });
587
+ }
588
+ // The classic sentence-length series (Drozdz et al.): 1/f rhythm across
589
+ // sentences is the literary signature; no memory across sentences is the
590
+ // flat one. Measured by the backend on every sentence, fragments included.
591
+ const sentenceSeries = fractal?.sentenceLength?.byWords;
592
+ if (sentenceSeries && confidenceFor(sentenceSeries.n) !== "none" && confidenceFor(sentenceSeries.n) !== "indicative") {
593
+ const band = rhythmBand(sentenceSeries.alphaBounded);
594
+ const direction = band === "random" ? 1 : band === "fractal" || band === "persistent" ? -1 : band ? -0.5 : 0;
595
+ if (direction !== 0)
596
+ evidence.push({
597
+ signal: "sentence-length rhythm across scales (DFA alpha)",
598
+ value: sentenceSeries.alphaBounded,
599
+ direction,
600
+ weight: 1,
601
+ reading: direction > 0
602
+ ? "Sentence lengths carry no rhythm from one to the next."
603
+ : "Sentence lengths follow a rhythm across scales, as in human prose.",
604
+ });
605
+ }
606
+ // Memory of the topic path: memoryless hopping vs. fractal return. Weighted
607
+ // below the sentence rhythm: generated prose with one topic per paragraph
608
+ // reads fractal here too, so the signal separates lists from prose more
609
+ // than machines from people.
610
+ const levels = [
611
+ ["statements", fractal?.statements?.byStepLength],
612
+ ["words", fractal?.words?.byStepLength],
613
+ ];
614
+ for (const [level, series] of levels) {
615
+ if (!series || confidenceFor(series.n) === "none" || confidenceFor(series.n) === "indicative")
616
+ continue;
617
+ const band = rhythmBand(series.alphaBounded);
618
+ if (!band)
619
+ continue;
620
+ const direction = band === "random" ? 1 : band === "fractal" || band === "persistent" ? -1 : 0;
621
+ if (direction === 0)
622
+ continue;
623
+ evidence.push({
624
+ signal: `${level}-level topic path memory (DFA alpha)`,
625
+ value: series.alphaBounded,
626
+ direction,
627
+ weight: 1,
628
+ reading: direction > 0
629
+ ? "The text hops between topics without memory of where it has been."
630
+ : "The text returns to its topics at every scale, as human narrative does.",
631
+ });
632
+ }
633
+ // Multifractality on the word path, only when the surrogate test is meaningful.
634
+ const mf = fractal?.words?.byStepLength?.multifractal;
635
+ if (mf && (fractal?.words?.byStepLength?.n ?? 0) >= 512 && mf.significant != null) {
636
+ const humanLike = mf.significant && mf.label === "multifractal" && mf.source !== "distribution";
637
+ const direction = humanLike ? -1 : mf.label === "monofractal" ? 0.5 : 0;
638
+ if (direction !== 0)
639
+ evidence.push({
640
+ signal: "multifractality of the topic path",
641
+ value: mf.label,
642
+ direction,
643
+ weight: 0.75,
644
+ reading: humanLike
645
+ ? "Calm stretches punctuated by rare big leaps: intermittent pacing typical of human writing."
646
+ : "Steady pacing with no scale-dependent leaps.",
647
+ });
648
+ }
649
+ // Burstiness of movement through the graph and regularity of topic changes.
650
+ const words = amplitude.words;
651
+ if (words && words.n >= 128) {
652
+ if (words.cvStep != null) {
653
+ const direction = words.cvStep < 0.5 ? 0.7 : words.cvStep > 0.9 ? -0.7 : 0;
654
+ if (direction !== 0)
655
+ evidence.push({
656
+ signal: "step-length variability of the topic path (CV)",
657
+ value: words.cvStep,
658
+ direction,
659
+ weight: 0.5,
660
+ reading: direction > 0
661
+ ? "Moves between ideas are all about the same size."
662
+ : "Mostly small moves with occasional long leaps.",
663
+ });
664
+ }
665
+ if (words.crossClusterRunCV != null) {
666
+ const direction = words.crossClusterRunCV < 0.7 ? 1 : words.crossClusterRunCV > 1.3 ? -0.5 : 0;
667
+ if (direction !== 0)
668
+ evidence.push({
669
+ signal: "regularity of topic changes",
670
+ value: words.crossClusterRunCV,
671
+ direction,
672
+ weight: 0.5,
673
+ reading: direction > 0
674
+ ? "Topic changes come at regular intervals, like items in an outline."
675
+ : "Topic changes come in bursts.",
676
+ });
677
+ }
678
+ }
679
+ const totalWeight = evidence.reduce((sum, e) => sum + e.weight, 0);
680
+ const score = totalWeight > 0 ? round(evidence.reduce((sum, e) => sum + e.weight * e.direction, 0) / totalWeight, 2) : null;
681
+ const enough = evidence.length >= 3 && sentenceRhythm.sentences >= 30;
682
+ const verdict = score == null || !enough ? "inconclusive" : score >= 0.25 ? "leans generated" : score <= -0.25 ? "leans human" : "inconclusive";
683
+ // Medium confidence only for a clear-cut profile with several agreeing
684
+ // signals; anything near the middle stays low.
685
+ const clearCut = score != null && Math.abs(score) >= 0.5;
686
+ return {
687
+ verdict,
688
+ score,
689
+ confidence: enough && clearCut && evidence.length >= 4 ? "medium" : "low",
690
+ evidence,
691
+ caveats: CAVEATS,
692
+ };
693
+ }
694
+ const NOTES = [
695
+ "Fractal levels are not comparable with each other: the word level reads lowest and the n-gram level higher by construction. Compare a level only with the same level of another text.",
696
+ "Amplitude is in units of the graph radius (RMS distance of the nodes from their centroid), so it is comparable across texts of different size.",
697
+ "sentence_rhythm counts prose statements only (fragments skipped); sentence_rhythm.scaling is the backend's DFA over every sentence of the text and says nothing about topics, only about pace. It degenerates on scripts written without spaces.",
698
+ "structure.degree_distribution describes how concentrated connectivity is in the whole network before the node cap. Its gini and tail exponent are descriptors, never a scale-free or power-law finding: word networks are heavy-tailed for any text.",
699
+ "rhythm.readings.influence is read on the betweenness-rank series; the raw byBetweenness alpha is biased upward by hub spikes.",
700
+ ];
701
+ export function buildTextSignature(input) {
702
+ const fractal = input.fractal ?? null;
703
+ const diversity = input.diversity ?? null;
704
+ const degreeDistribution = input.degreeDistribution ? trimDegreeDistribution(input.degreeDistribution) : null;
705
+ const amplitude = computeAmplitude(input.nodes, input.statements);
706
+ const sentenceRhythm = computeSentenceRhythm(input.statements);
707
+ return {
708
+ signature: composeSignature(diversity, fractal),
709
+ structure: {
710
+ modularity: input.modularity ?? null,
711
+ diversity_stats: diversity,
712
+ degree_distribution: degreeDistribution,
713
+ readings: readStructure(diversity),
714
+ degreeReadings: readDegreeDistribution(degreeDistribution),
715
+ },
716
+ rhythm: {
717
+ fractal_variability: fractal,
718
+ readings: {
719
+ statements: readLevel(fractal?.statements),
720
+ words: readLevel(fractal?.words),
721
+ ngrams: readLevel(fractal?.ngrams),
722
+ influence: readInfluence(fractal),
723
+ },
724
+ },
725
+ amplitude: {
726
+ ...amplitude,
727
+ readings: { statements: readAmplitude(amplitude.statements), words: readAmplitude(amplitude.words) },
728
+ },
729
+ sentence_rhythm: {
730
+ ...sentenceRhythm,
731
+ readings: readSentenceRhythm(sentenceRhythm),
732
+ scaling: summariseSentenceScaling(fractal),
733
+ },
734
+ ai_likeness: estimateAiLikeness({ sentenceRhythm, amplitude, fractal }),
735
+ notes: NOTES,
736
+ };
737
+ }
738
+ //# sourceMappingURL=textSignature.js.map