dsh-plugin-show-me-data 0.1.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 (130) hide show
  1. package/LICENSE +27 -0
  2. package/README.md +96 -0
  3. package/cordis.patch.yml +40 -0
  4. package/docs/01-product-effect.md +178 -0
  5. package/docs/02-architecture.md +275 -0
  6. package/docs/03-data-contracts.md +291 -0
  7. package/docs/04-sources.md +342 -0
  8. package/docs/05-ui-spec.md +167 -0
  9. package/docs/06-ai-layer.md +194 -0
  10. package/docs/07-implementation-plan.md +399 -0
  11. package/docs/08-test-plan.md +133 -0
  12. package/docs/09-packaging-install.md +249 -0
  13. package/docs/10-kickoff-prompt.md +94 -0
  14. package/docs/11-decisions.md +203 -0
  15. package/docs/12-runtime-verified.md +115 -0
  16. package/docs/13-acceptance.md +153 -0
  17. package/docs/14-progress.md +150 -0
  18. package/docs/15-publish.md +185 -0
  19. package/lib/app/ai-deterministic.js +327 -0
  20. package/lib/app/ai-validate.js +284 -0
  21. package/lib/app/ai.js +440 -0
  22. package/lib/app/health.js +77 -0
  23. package/lib/app/overview.js +349 -0
  24. package/lib/app/propose-indicator.js +122 -0
  25. package/lib/app/refresh.js +251 -0
  26. package/lib/app/series-view.js +195 -0
  27. package/lib/app/watchlist.js +102 -0
  28. package/lib/client.js +4322 -0
  29. package/lib/core/ai/prompts.js +213 -0
  30. package/lib/core/chart/axis.js +133 -0
  31. package/lib/core/chart/bar.js +58 -0
  32. package/lib/core/chart/candle.js +216 -0
  33. package/lib/core/chart/line.js +186 -0
  34. package/lib/core/chart/scale.js +132 -0
  35. package/lib/core/format.js +143 -0
  36. package/lib/core/indicators/catalog.js +1011 -0
  37. package/lib/core/indicators/resolve.js +196 -0
  38. package/lib/core/insight/digest.js +250 -0
  39. package/lib/core/insight/rank.js +115 -0
  40. package/lib/core/insight/related.js +90 -0
  41. package/lib/core/insight/rules.js +417 -0
  42. package/lib/core/stats/derive.js +123 -0
  43. package/lib/core/stats/series.js +465 -0
  44. package/lib/core/time/range.js +242 -0
  45. package/lib/core/types.js +478 -0
  46. package/lib/host/ai/discussion.js +559 -0
  47. package/lib/host/ai/dsh-llm-gateway.js +333 -0
  48. package/lib/host/config.js +194 -0
  49. package/lib/host/http/respond.js +165 -0
  50. package/lib/host/http/routes.js +689 -0
  51. package/lib/host/index.js +293 -0
  52. package/lib/host/infra/fs-repos.js +179 -0
  53. package/lib/host/infra/memory-fallback.js +64 -0
  54. package/lib/host/tools/define-tool.js +295 -0
  55. package/lib/host/tools/register.js +431 -0
  56. package/lib/host.js +7 -0
  57. package/lib/ports/clock.js +57 -0
  58. package/lib/ports/snapshot-repo.js +48 -0
  59. package/lib/sources/eastmoney-macro.js +197 -0
  60. package/lib/sources/eastmoney-quote.js +201 -0
  61. package/lib/sources/ecb.js +179 -0
  62. package/lib/sources/fred.js +207 -0
  63. package/lib/sources/http.js +136 -0
  64. package/lib/sources/ohlc.js +36 -0
  65. package/lib/sources/quote-cascade.js +177 -0
  66. package/lib/sources/registry.js +153 -0
  67. package/lib/sources/sina-cn.js +197 -0
  68. package/lib/sources/sina-us.js +187 -0
  69. package/lib/sources/tencent.js +158 -0
  70. package/lib/sources/us-treasury-rates.js +275 -0
  71. package/lib/sources/us-treasury.js +196 -0
  72. package/lib/sources/worldbank.js +170 -0
  73. package/package.json +69 -0
  74. package/src/app/ai-deterministic.js +327 -0
  75. package/src/app/ai-validate.js +284 -0
  76. package/src/app/ai.js +440 -0
  77. package/src/app/health.js +77 -0
  78. package/src/app/overview.js +349 -0
  79. package/src/app/propose-indicator.js +122 -0
  80. package/src/app/refresh.js +251 -0
  81. package/src/app/series-view.js +195 -0
  82. package/src/app/watchlist.js +102 -0
  83. package/src/client/api.js +323 -0
  84. package/src/client/components.js +1877 -0
  85. package/src/client/copy.js +368 -0
  86. package/src/client/index.js +169 -0
  87. package/src/client/store.js +219 -0
  88. package/src/core/ai/prompts.js +213 -0
  89. package/src/core/chart/axis.js +133 -0
  90. package/src/core/chart/bar.js +58 -0
  91. package/src/core/chart/candle.js +216 -0
  92. package/src/core/chart/line.js +186 -0
  93. package/src/core/chart/scale.js +132 -0
  94. package/src/core/format.js +143 -0
  95. package/src/core/indicators/catalog.js +1011 -0
  96. package/src/core/indicators/resolve.js +196 -0
  97. package/src/core/insight/digest.js +250 -0
  98. package/src/core/insight/rank.js +115 -0
  99. package/src/core/insight/related.js +90 -0
  100. package/src/core/insight/rules.js +417 -0
  101. package/src/core/stats/derive.js +123 -0
  102. package/src/core/stats/series.js +465 -0
  103. package/src/core/time/range.js +242 -0
  104. package/src/core/types.js +478 -0
  105. package/src/host/ai/discussion.js +559 -0
  106. package/src/host/ai/dsh-llm-gateway.js +333 -0
  107. package/src/host/config.js +194 -0
  108. package/src/host/http/respond.js +165 -0
  109. package/src/host/http/routes.js +689 -0
  110. package/src/host/index.js +293 -0
  111. package/src/host/infra/fs-repos.js +179 -0
  112. package/src/host/infra/memory-fallback.js +64 -0
  113. package/src/host/tools/define-tool.js +295 -0
  114. package/src/host/tools/register.js +431 -0
  115. package/src/ports/clock.js +57 -0
  116. package/src/ports/snapshot-repo.js +48 -0
  117. package/src/sources/eastmoney-macro.js +197 -0
  118. package/src/sources/eastmoney-quote.js +201 -0
  119. package/src/sources/ecb.js +179 -0
  120. package/src/sources/fred.js +207 -0
  121. package/src/sources/http.js +136 -0
  122. package/src/sources/ohlc.js +36 -0
  123. package/src/sources/quote-cascade.js +177 -0
  124. package/src/sources/registry.js +153 -0
  125. package/src/sources/sina-cn.js +197 -0
  126. package/src/sources/sina-us.js +187 -0
  127. package/src/sources/tencent.js +158 -0
  128. package/src/sources/us-treasury-rates.js +275 -0
  129. package/src/sources/us-treasury.js +196 -0
  130. package/src/sources/worldbank.js +170 -0
@@ -0,0 +1,295 @@
1
+ /**
2
+ * A dependency-free 'ToolDefinition' builder (docs/09 §4).
3
+ *
4
+ * The plugin deliberately does **not** import '@deepseek-ai/dsh-tools''s
5
+ * 'defineTool': this package is installed from the session workspace where the
6
+ * profile's 'node_modules' is not resolvable, and pulling a second copy of
7
+ * 'dsh-tools' into the process would fork the tool runtime. Building the
8
+ * definition shape by hand keeps the plugin dependency-free — the schema subset
9
+ * and the argument validation it would have provided are implemented here and
10
+ * covered by 'test/host/tools.test.js'.
11
+ *
12
+ * @module host/tools/define-tool
13
+ */
14
+
15
+ /** JSON Schema keywords the registry enforces. */
16
+ const ALLOWED_TYPES = ['object', 'array', 'string', 'number', 'integer', 'boolean', 'null']
17
+
18
+ /**
19
+ * Validate a raw JSON Schema node against the enforced subset.
20
+ *
21
+ * @param {any} schema - candidate schema.
22
+ * @param {string} path - path for error messages.
23
+ * @returns {string[]} violations (empty when supported).
24
+ */
25
+ export function schemaViolations(schema, path = 'schema') {
26
+ const violations = []
27
+ if (schema === null || typeof schema !== 'object' || Array.isArray(schema)) {
28
+ return [`${path} must be an object`]
29
+ }
30
+ if (schema.type !== undefined && !ALLOWED_TYPES.includes(schema.type)) {
31
+ violations.push(`${path}.type must be one of ${ALLOWED_TYPES.join('/')}`)
32
+ }
33
+ if (schema.properties !== undefined) {
34
+ if (schema.type !== 'object') violations.push(`${path}.properties requires type: 'object'`)
35
+ if (schema.properties === null || typeof schema.properties !== 'object') {
36
+ violations.push(`${path}.properties must be an object`)
37
+ } else {
38
+ for (const [key, child] of Object.entries(schema.properties)) {
39
+ violations.push(...schemaViolations(child, `${path}.properties.${key}`))
40
+ }
41
+ }
42
+ }
43
+ if (schema.required !== undefined) {
44
+ if (!Array.isArray(schema.required) || schema.required.some((entry) => typeof entry !== 'string')) {
45
+ violations.push(`${path}.required must be an array of strings`)
46
+ }
47
+ }
48
+ if (schema.items !== undefined) {
49
+ if (schema.type !== 'array') violations.push(`${path}.items requires type: 'array'`)
50
+ violations.push(...schemaViolations(schema.items, `${path}.items`))
51
+ } else if (schema.type === 'array') {
52
+ // An array without `items` is legal (unconstrained entries) but almost always
53
+ // an authoring slip, so it is reported as a warning-level violation only when
54
+ // the schema also declares nothing else. We keep it legal: several tools
55
+ // return genuinely heterogeneous arrays.
56
+ }
57
+ if (schema.enum !== undefined && !Array.isArray(schema.enum)) {
58
+ violations.push(`${path}.enum must be an array`)
59
+ }
60
+ if (schema.oneOf !== undefined) {
61
+ if (!Array.isArray(schema.oneOf) || schema.oneOf.length !== 1) {
62
+ // The registry's subset allows exactly-one `oneOf`; we never emit it, so a
63
+ // malformed one here is an author error worth reporting.
64
+ violations.push(`${path}.oneOf must be an array with exactly one entry`)
65
+ } else {
66
+ violations.push(...schemaViolations(schema.oneOf[0], `${path}.oneOf[0]`))
67
+ }
68
+ }
69
+ const known = new Set(['type', 'properties', 'required', 'additionalProperties', 'items', 'enum', 'const', 'oneOf', 'description', 'title', 'default', 'examples'])
70
+ for (const key of Object.keys(schema)) {
71
+ if (!known.has(key)) violations.push(`${path}.${key} is not a supported keyword`)
72
+ }
73
+ return violations
74
+ }
75
+
76
+ /**
77
+ * Validate a value against the JSON Schema subset.
78
+ *
79
+ * @param {any} schema - schema.
80
+ * @param {any} value - candidate value.
81
+ * @param {string} [path] - path for error messages.
82
+ * @returns {string[]} violations.
83
+ */
84
+ export function valueViolations(schema, value, path = '') {
85
+ const violations = []
86
+ if (schema === undefined || schema === null) return violations
87
+ if (schema.oneOf !== undefined) {
88
+ const passes = schema.oneOf.filter((candidate) => valueViolations(candidate, value, path).length === 0)
89
+ if (passes.length !== 1) violations.push(`${path || 'value'} must match exactly one allowed shape`)
90
+ return violations
91
+ }
92
+ if (schema.enum !== undefined && !schema.enum.includes(value)) {
93
+ violations.push(`${path || 'value'} must be one of ${schema.enum.map((entry) => JSON.stringify(entry)).join(', ')}`)
94
+ return violations
95
+ }
96
+ if (schema.const !== undefined && value !== schema.const) {
97
+ violations.push(`${path || 'value'} must be ${JSON.stringify(schema.const)}`)
98
+ return violations
99
+ }
100
+ const type = schema.type
101
+ if (type === 'object') {
102
+ if (value === null || typeof value !== 'object' || Array.isArray(value)) {
103
+ violations.push(`${path || 'value'} must be an object`)
104
+ return violations
105
+ }
106
+ for (const key of schema.required ?? []) {
107
+ if (!(key in value)) violations.push(`${path ? `${path}.` : ''}${key} is required`)
108
+ }
109
+ const properties = schema.properties ?? {}
110
+ if (schema.additionalProperties === false) {
111
+ for (const key of Object.keys(value)) {
112
+ if (!(key in properties)) violations.push(`${path ? `${path}.` : ''}${key} is not allowed`)
113
+ }
114
+ }
115
+ for (const [key, child] of Object.entries(properties)) {
116
+ if (key in value) violations.push(...valueViolations(child, value[key], `${path ? `${path}.` : ''}${key}`))
117
+ }
118
+ return violations
119
+ }
120
+ if (type === 'array') {
121
+ if (!Array.isArray(value)) {
122
+ violations.push(`${path || 'value'} must be an array`)
123
+ return violations
124
+ }
125
+ if (schema.items !== undefined) {
126
+ value.forEach((entry, index) => violations.push(...valueViolations(schema.items, entry, `${path}[${index}]`)))
127
+ }
128
+ return violations
129
+ }
130
+ if (type === 'string' && typeof value !== 'string') violations.push(`${path || 'value'} must be a string`)
131
+ if (type === 'number' && (typeof value !== 'number' || !Number.isFinite(value))) violations.push(`${path || 'value'} must be a finite number`)
132
+ if (type === 'integer' && !Number.isInteger(value)) violations.push(`${path || 'value'} must be an integer`)
133
+ if (type === 'boolean' && typeof value !== 'boolean') violations.push(`${path || 'value'} must be a boolean`)
134
+ if (type === 'null' && value !== null) violations.push(`${path || 'value'} must be null`)
135
+ return violations
136
+ }
137
+
138
+ /**
139
+ * Drop `undefined` object properties and array holes from a tool value.
140
+ *
141
+ * Arrays keep their length (a hole becomes `null`) so a point series never
142
+ * silently shifts; objects simply lose the absent keys, which is what
143
+ * `JSON.stringify` would have done anyway — except that the harness snapshots
144
+ * the live value, where an own property set to `undefined` is an error rather
145
+ * than an omission.
146
+ *
147
+ * @param {any} value - candidate value.
148
+ * @returns {any} a JSON-safe copy.
149
+ */
150
+ export function dropUndefined(value) {
151
+ if (value === null || typeof value !== 'object') return value
152
+ if (Array.isArray(value)) {
153
+ return value.map((entry) => (entry === undefined ? null : dropUndefined(entry)))
154
+ }
155
+ /** @type {Record<string, any>} */
156
+ const out = {}
157
+ for (const [key, entry] of Object.entries(value)) {
158
+ if (entry === undefined) continue
159
+ out[key] = dropUndefined(entry)
160
+ }
161
+ return out
162
+ }
163
+
164
+ /**
165
+ * Find the first value the harness's lossless-JSON boundary would reject.
166
+ *
167
+ * A tool body's canonical value is snapshotted by the registry before it is
168
+ * stored, rendered or sent to the model, and that snapshot accepts only ordinary
169
+ * arrays, plain objects and JSON scalars: `undefined`, `NaN`, `Infinity`,
170
+ * `-0`, sparse arrays, `Date`, class instances and cycles are all refused. When
171
+ * that happens the model sees nothing but `value is not lossless JSON`, with no
172
+ * hint of which field caused it — so the plugin checks the same rule first and
173
+ * names the offending path itself.
174
+ *
175
+ * @param {any} value - candidate value.
176
+ * @param {string} [path] - path of `value`, for the message.
177
+ * @param {Set<object>} [seen] - ancestors on the current path.
178
+ * @returns {string|undefined} the offending path.
179
+ */
180
+ export function findNonLossless(value, path = 'value', seen = new Set()) {
181
+ if (value === null) return undefined
182
+ const type = typeof value
183
+ if (type === 'string' || type === 'boolean') return undefined
184
+ if (type === 'number') {
185
+ if (!Number.isFinite(value)) return `${path} is ${String(value)} (neither is JSON)`
186
+ if (Object.is(value, -0)) return `${path} is -0 (not lossless)`
187
+ return undefined
188
+ }
189
+ if (type === 'undefined') return `${path} is undefined (dropped by JSON, refused at the boundary)`
190
+ if (type === 'bigint' || type === 'function' || type === 'symbol') return `${path} is a ${type}`
191
+ if (seen.has(value)) return `${path} is a cycle`
192
+ seen.add(value)
193
+ try {
194
+ if (Array.isArray(value)) {
195
+ for (let index = 0; index < value.length; index += 1) {
196
+ if (!Object.prototype.hasOwnProperty.call(value, index)) return `${path}[${index}] is a hole (sparse array)`
197
+ const found = findNonLossless(value[index], `${path}[${index}]`, seen)
198
+ if (found !== undefined) return found
199
+ }
200
+ return undefined
201
+ }
202
+ const prototype = Object.getPrototypeOf(value)
203
+ if (prototype !== Object.prototype && prototype !== null) {
204
+ return `${path} is a ${value?.constructor?.name ?? 'non-plain object'} (only plain objects survive)`
205
+ }
206
+ for (const key of Object.keys(value)) {
207
+ const found = findNonLossless(value[key], `${path}.${key}`, seen)
208
+ if (found !== undefined) return found
209
+ }
210
+ return undefined
211
+ } finally {
212
+ seen.delete(value)
213
+ }
214
+ }
215
+
216
+ /**
217
+ * Build a 'ToolDefinition' the registry accepts.
218
+ *
219
+ * @param {object} options - tool options.
220
+ * @param {string} options.name - tool name.
221
+ * @param {string} options.description - model-facing description.
222
+ * @param {Record<string, any>} options.parameters - argument JSON Schema (object root).
223
+ * @param {any} options.outputSchema - canonical output JSON Schema.
224
+ * @param {(args: any, value: any) => Array<{ type: string, text: string }>} options.render - content projection.
225
+ * @param {(args: any, exec: any) => Promise<any>} options.execute - tool body.
226
+ * @param {number} [options.timeoutMs] - cooperative timeout budget.
227
+ * @param {boolean} [options.concurrencySafe] - whether parallel dispatch is allowed.
228
+ * @returns {object} tool definition.
229
+ */
230
+ export function defineTool({ name, description, parameters, outputSchema, render, execute, timeoutMs, concurrencySafe }) {
231
+ const parameterViolations = schemaViolations(parameters, `${name}.parameters`)
232
+ if (parameters?.type !== 'object') parameterViolations.push(`${name}.parameters must be an object schema`)
233
+ const outputViolations = schemaViolations(outputSchema, `${name}.output.schema`)
234
+ const violations = [...parameterViolations, ...outputViolations]
235
+ if (violations.length > 0) throw new Error(`defineTool(${name}): ${violations.join('; ')}`)
236
+ if (typeof render !== 'function') throw new Error(`defineTool(${name}): output.render is required`)
237
+ if (typeof execute !== 'function') throw new Error(`defineTool(${name}): execute is required`)
238
+
239
+ const definition = {
240
+ name,
241
+ description,
242
+ parameters,
243
+ output: {
244
+ schema: outputSchema,
245
+ render(args, value) {
246
+ return render(args, value)
247
+ },
248
+ },
249
+ async execute(args, exec) {
250
+ const argsViolations = valueViolations(parameters, args, '')
251
+ if (argsViolations.length > 0) {
252
+ const error = new Error(`invalid arguments: ${argsViolations.join('; ')}`)
253
+ error.name = 'ToolArgsError'
254
+ throw error
255
+ }
256
+ const raw = await execute(args ?? {}, exec)
257
+ // `undefined` is not a JSON value, but it is easy to produce: an optional
258
+ // field that simply is not there (`compare`, `error`, `measuredAt`) used to
259
+ // travel as an own property set to undefined, and the harness's snapshot
260
+ // then rejected the *whole* tool result with "value is not lossless JSON" —
261
+ // the model got no data at all and no way to tell why. Drop those keys
262
+ // first, then validate what is left.
263
+ const value = dropUndefined(raw)
264
+ const valueViolationsFound = valueViolations(outputSchema, value, '')
265
+ if (valueViolationsFound.length > 0) {
266
+ const error = new Error(`tool ${name} violated its declared output: ${valueViolationsFound.join('; ')}`)
267
+ error.name = 'ToolOutputError'
268
+ throw error
269
+ }
270
+ const nonLossless = findNonLossless(value, `${name}.output`)
271
+ if (nonLossless !== undefined) {
272
+ const error = new Error(`tool ${name} returned a value the harness cannot snapshot: ${nonLossless}`)
273
+ error.name = 'ToolOutputError'
274
+ throw error
275
+ }
276
+ return value
277
+ },
278
+ }
279
+ if (typeof timeoutMs === 'number') definition.timeoutMs = timeoutMs
280
+ definition.isConcurrencySafe = () => concurrencySafe === true
281
+ return definition
282
+ }
283
+
284
+ /**
285
+ * Render one JSON tool result as model-facing text, keeping it readable in a
286
+ * transcript rather than dumping raw JSON for every call.
287
+ *
288
+ * @param {any} value - canonical value.
289
+ * @param {number} [maxChars] - truncation budget.
290
+ * @returns {string} text.
291
+ */
292
+ export function summarize(value, maxChars = 4000) {
293
+ const text = typeof value === 'string' ? value : JSON.stringify(value, null, 2)
294
+ return text.length <= maxChars ? text : `${text.slice(0, maxChars)}\n… (结果已截断,完整内容见工具卡片)`
295
+ }