@v587d/capital-generation 2.15.0 → 2.16.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 (64) hide show
  1. package/CHANGELOG.md +171 -0
  2. package/README.md +7 -12
  3. package/THIRD_PARTY_NOTICES.md +1 -0
  4. package/automation/TABLER-ICONS-LICENSE +21 -0
  5. package/automation/builtins.js +237 -10
  6. package/automation/client.js +79 -17
  7. package/automation/client.src.cjs +873 -210
  8. package/automation/contract.js +4 -0
  9. package/automation/defaults.js +6 -4
  10. package/automation/index.js +12 -7
  11. package/automation/package.json +5 -2
  12. package/automation/route.js +9 -2
  13. package/automation/runner.js +33 -6
  14. package/automation/scheduler.js +34 -29
  15. package/automation/schema.js +39 -5
  16. package/automation/service.js +102 -10
  17. package/automation/settings.js +6 -0
  18. package/automation/store.js +64 -21
  19. package/automation/task-icons.js +75 -0
  20. package/automation/workspaces.js +113 -13
  21. package/chart-ui/client.js +2 -2
  22. package/chart-ui/package.json +1 -1
  23. package/config/client.js +19 -5
  24. package/config/client.src.cjs +365 -44
  25. package/config/index.js +7 -5
  26. package/config/package.json +1 -1
  27. package/lib/agents/root-tool-policy.d.ts +3 -3
  28. package/lib/agents/root-tool-policy.js +4 -2
  29. package/lib/chart/runtime.d.ts +1 -1
  30. package/lib/chart/runtime.js +95 -0
  31. package/lib/chart/series.d.ts +4 -4
  32. package/lib/chart/series.js +44 -10
  33. package/lib/data-collector/dataset-tools.js +22 -0
  34. package/lib/data-collector/describe.js +1 -0
  35. package/lib/data-collector/hub.js +5 -0
  36. package/lib/data-collector/store.d.ts +5 -0
  37. package/lib/data-collector/store.js +17 -0
  38. package/lib/data-collector/time-axis.d.ts +15 -0
  39. package/lib/data-collector/time-axis.js +73 -0
  40. package/lib/data-collector/tools.d.ts +1 -1
  41. package/lib/data-collector/tools.js +12 -1
  42. package/lib/index.d.ts +5 -0
  43. package/lib/index.js +7 -5
  44. package/lib/sources/tencent-http.js +7 -2
  45. package/lib/sources/tencent-public-core.d.ts +2 -0
  46. package/lib/sources/tencent-public-core.js +5 -0
  47. package/lib/sources/wind-mcp.js +3 -3
  48. package/lib/sources-entry.d.ts +1 -1
  49. package/lib/web-retriever/local-fetch.d.ts +1 -1
  50. package/lib/web-retriever/local-fetch.js +1 -1
  51. package/lib/web-retriever/sources.d.ts +58 -0
  52. package/lib/web-retriever/sources.js +290 -0
  53. package/lib/web-retriever/tools.js +199 -8
  54. package/lib/web-retriever/wind-client.d.ts +1 -1
  55. package/lib/web-retriever/wind-client.js +1 -1
  56. package/package.json +1 -1
  57. package/preset/capital-generation/agent.patch.yml +20 -19
  58. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -3
  59. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +9 -2
  60. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +2 -1
  61. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +2 -0
  62. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +18 -13
  63. package/preset/capital-generation/skills/capital-web-protocol/SKILL.md +30 -15
  64. package/watchlist/package.json +1 -1
@@ -4,12 +4,14 @@
4
4
  */
5
5
  import { CAPITAL_PRESET_IDS, ROOT_PRESET_ID } from '../preset-ids.js';
6
6
  import { readPresetClaim } from '../preset-claim.js';
7
- /** Outbound tools (search 2 + wind 2 + named sources 9); `ocr` is excluded because only its url form is restricted. */
7
+ /** Outbound tools (search 2 + wind 2 + named sources 11); `ocr` is excluded because only its url form is restricted. */
8
8
  export const RETRIEVAL_DENIED_TOOLS = [
9
9
  'anysearch_search', 'web_retriever_fetch',
10
10
  'wind_docs_announcements', 'wind_docs_news',
11
11
  'cls_telegraph', 'wscn_lives', 'cninfo_irm', 'sseinfo_qa',
12
- 'eastmoney_724', 'eastmoney_stock_news', 'eastmoney_reports', 'sina_reports', 'ths_eps_forecast',
12
+ 'eastmoney_724', 'eastmoney_stock_news', 'eastmoney_reports',
13
+ 'eastmoney_announcements', 'eastmoney_announcement_body',
14
+ 'sina_reports', 'ths_eps_forecast',
13
15
  ];
14
16
  export const OCR_TOOL_NAME = 'ocr';
15
17
  /** Name-level deny cannot express the form distinction; this is a call-level guard. */
@@ -4,4 +4,4 @@
4
4
  * back to `globalThis.LightweightCharts` — another plugin's copy could silently win. The IIFE
5
5
  * returns `{ render, version }`; nothing is attached to globals.
6
6
  */
7
- export declare const CHART_RUNTIME_SOURCE = "\n(function () {\n 'use strict'\n\n var FALLBACK = {\n '--dsw-alias-bg-base': '#ffffff',\n '--dsw-alias-border-l1': 'rgba(0, 0, 0, 0.10)',\n '--dsw-alias-label-primary': '#1f2328',\n '--dsw-alias-label-secondary': '#6b7280',\n '--dsw-alias-brand-primary': '#2563eb',\n '--dsw-alias-state-error-primary': '#d94a3d',\n '--dsw-alias-state-success-primary': '#2f9e44'\n }\n\n // Hex colors receive alpha; other formats convert to rgba or stay unchanged, never emit invalid CSS.\n function withAlpha(color, alpha) {\n var text = String(color || '').trim()\n var hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(text)\n if (hex) {\n var expanded = hex[1].length === 3\n ? '#' + hex[1].split('').map(function (part) { return part + part }).join('')\n : text\n return expanded + Math.round(alpha * 255).toString(16).padStart(2, '0')\n }\n var rgb = /^rgba?\\(\\s*([^)]*)\\)$/i.exec(text)\n if (rgb) {\n var parts = rgb[1].split(/[,\\s/]+/).filter(Boolean)\n if (parts.length >= 3 && parts.slice(0, 3).every(function (part) { return /^[-+]?\\d*\\.?\\d+%?$/.test(part) })) {\n return 'rgba(' + parts.slice(0, 3).join(', ') + ', ' + alpha + ')'\n }\n }\n return text\n }\n\n // First series uses brand color; later series use a stable palette so one symbol keeps its color.\n var PALETTE = ['#f59e0b', '#10b981', '#8b5cf6', '#0ea5e9', '#ec4899']\n\n function readToken(name) {\n try {\n var value = getComputedStyle(document.documentElement).getPropertyValue(name)\n if (value && value.trim().length > 0) return value.trim()\n } catch (error) {\n /* Non-browser or unavailable styles fall back to defaults. */\n }\n return FALLBACK[name]\n }\n\n function resolveColors() {\n return {\n bg: readToken('--dsw-alias-bg-base'),\n grid: readToken('--dsw-alias-border-l1'),\n text: readToken('--dsw-alias-label-secondary'),\n strong: readToken('--dsw-alias-label-primary'),\n brand: readToken('--dsw-alias-brand-primary'),\n up: readToken('--dsw-alias-state-error-primary'),\n down: readToken('--dsw-alias-state-success-primary')\n }\n }\n\n function showError(container, message) {\n container.textContent = ''\n var box = document.createElement('div')\n box.setAttribute('data-chart-error', '1')\n box.style.cssText = 'padding:12px;border:0.5px solid ' + readToken('--dsw-alias-border-l1') + ';border-radius:8px;color:' + readToken('--dsw-alias-label-secondary') + ';font-size:13px;line-height:1.6'\n box.textContent = message\n container.appendChild(box)\n }\n\n function applyIndexLabels(chart, labels) {\n function format(time) {\n var key = String(time)\n return Object.prototype.hasOwnProperty.call(labels, key) ? labels[key] : key\n }\n // Replace both tick and crosshair formatters for category axes or synthetic dates leak through.\n chart.applyOptions({\n localization: { timeFormatter: format },\n timeScale: { tickMarkFormatter: function (time) { return format(time) } }\n })\n }\n\n /** Support both v5 plugin markers and the older series API so markers never disappear silently. */\n function attachMarkers(anchor, markers, LWC) {\n if (typeof LWC.createSeriesMarkers === 'function') {\n LWC.createSeriesMarkers(anchor, markers)\n return true\n }\n if (anchor && typeof anchor.setMarkers === 'function') {\n anchor.setMarkers(markers)\n return true\n }\n return false\n }\n\n function volumePointColor(point, colors) {\n if (point.direction === 'up') return colors.up\n if (point.direction === 'down') return colors.down\n return colors.brand\n }\n\n function seriesColor(index, colors) {\n return index === 0 ? colors.brand : PALETTE[(index - 1) % PALETTE.length]\n }\n\n function appendLegend(container, payload, colors) {\n var items = []\n if (payload.ohlc && payload.ohlc.label) items.push({ label: payload.ohlc.label, color: colors.up })\n if (Array.isArray(payload.series)) {\n payload.series.forEach(function (item, index) {\n if (item && item.label) items.push({ label: item.label, color: seriesColor(index, colors) })\n })\n }\n if (payload.volume && payload.volume.label) items.push({ label: payload.volume.label, color: colors.brand })\n if (items.length === 0) return\n\n var legend = document.createElement('div')\n legend.setAttribute('data-chart-legend', '1')\n legend.style.cssText = 'position:absolute;z-index:2;top:8px;left:8px;right:8px;display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;pointer-events:none;color:' + colors.text + ';font-size:11px;line-height:16px'\n items.forEach(function (item) {\n var entry = document.createElement('span')\n entry.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:1px 5px;border-radius:4px;background:' + withAlpha(colors.bg, 0.8)\n var swatch = document.createElement('span')\n swatch.style.cssText = 'width:7px;height:7px;border-radius:50%;background:' + item.color + ';flex:none'\n var label = document.createElement('span')\n label.textContent = item.label\n entry.appendChild(swatch)\n entry.appendChild(label)\n legend.appendChild(entry)\n })\n container.appendChild(legend)\n }\n\n function renderCapitalChart(container, payload, library) {\n if (!container) throw new Error('renderCapitalChart: container is required')\n if (!payload || typeof payload !== 'object') throw new Error('renderCapitalChart: payload is required')\n // Use the injected library; never fall back to a global copy owned by another plugin.\n var LWC = library\n if (!LWC || typeof LWC.createChart !== 'function') {\n showError(container, '\u56FE\u8868\u5E93\u672A\u52A0\u8F7D\uFF08\u8C03\u7528\u65B9\u672A\u63D0\u4F9B LightweightCharts\uFF09\u3002')\n return null\n }\n\n var colors = resolveColors()\n container.textContent = ''\n if (container.style) container.style.position = 'relative'\n container.setAttribute('data-chart-id', String(payload.chart_id || ''))\n container.setAttribute('data-chart-kind', String(payload.kind || ''))\n\n var chart = LWC.createChart(container, {\n autoSize: true,\n // attributionLogo stays enabled for Apache-2.0 attribution.\n layout: { background: { color: 'transparent' }, textColor: colors.text, fontFamily: 'inherit', attributionLogo: true },\n grid: { vertLines: { color: colors.grid }, horzLines: { color: colors.grid } },\n rightPriceScale: { borderColor: colors.grid, entireTextOnly: true },\n timeScale: { borderColor: colors.grid, rightOffset: 2, barSpacing: 6, minBarSpacing: 0.5 },\n crosshair: { mode: LWC.CrosshairMode ? LWC.CrosshairMode.Normal : 0 },\n localization: { locale: 'zh-CN' }\n })\n\n if (payload.axis === 'index' && payload.labels) applyIndexLabels(chart, payload.labels)\n\n var anchor = null\n\n if (payload.ohlc && Array.isArray(payload.ohlc.data) && payload.ohlc.data.length > 0) {\n // kind=bar uses BarSeries; other OHLC kinds use CandlestickSeries with the same data contract.\n var isOhlcBar = payload.kind === 'bar'\n var candles = chart.addSeries(isOhlcBar ? LWC.BarSeries : LWC.CandlestickSeries, {\n upColor: colors.up,\n downColor: colors.down,\n borderUpColor: colors.up,\n borderDownColor: colors.down,\n wickUpColor: colors.up,\n wickDownColor: colors.down\n })\n candles.setData(payload.ohlc.data)\n anchor = candles\n }\n\n if (Array.isArray(payload.series)) {\n for (var index = 0; index < payload.series.length; index += 1) {\n var item = payload.series[index]\n if (!item || !Array.isArray(item.data) || item.data.length === 0) continue\n var color = seriesColor(index, colors)\n var options = { title: item.label || '', priceLineVisible: false, lastValueVisible: true }\n var series = null\n if (item.type === 'area') {\n options.lineColor = color\n options.topColor = withAlpha(color, 0.2)\n options.bottomColor = withAlpha(color, 0.02)\n options.lineWidth = 2\n series = chart.addSeries(LWC.AreaSeries, options)\n } else if (item.type === 'column') {\n // Single-value histograms need HistogramSeries; BarSeries would render an empty chart.\n options.color = color\n options.base = 0\n series = chart.addSeries(LWC.HistogramSeries, options)\n } else {\n options.color = color\n options.lineWidth = 2\n series = chart.addSeries(LWC.LineSeries, options)\n }\n series.setData(item.data)\n if (anchor === null) anchor = series\n }\n }\n\n if (payload.volume && Array.isArray(payload.volume.data) && payload.volume.data.length > 0) {\n var volume = chart.addSeries(LWC.HistogramSeries, {\n title: payload.volume.label || '',\n priceFormat: { type: 'volume' },\n priceLineVisible: false,\n lastValueVisible: false\n }, 1)\n volume.setData(payload.volume.data.map(function (point) {\n return { time: point.time, value: point.value, color: volumePointColor(point, colors) }\n }))\n }\n\n if (anchor === null) {\n showError(container, '\u8FD9\u5F20\u56FE\u6CA1\u6709\u53EF\u6E32\u67D3\u7684\u5E8F\u5217\uFF08payload \u91CC\u65E2\u6CA1\u6709 ohlc \u4E5F\u6CA1\u6709 series\uFF09\u3002')\n return chart\n }\n\n if (Array.isArray(payload.markers) && payload.markers.length > 0) {\n attachMarkers(anchor, payload.markers.map(function (marker) {\n return { time: marker.time, position: marker.position, text: marker.text, color: colors.strong, shape: 'circle' }\n }), LWC)\n }\n\n appendLegend(container, payload, colors)\n\n // Main/volume panes use a 3:1 ratio; a single pane has no second pane object.\n var panes = typeof chart.panes === 'function' ? chart.panes() : []\n if (panes.length > 1) {\n if (typeof panes[0].setStretchFactor === 'function') panes[0].setStretchFactor(3)\n if (typeof panes[1].setStretchFactor === 'function') panes[1].setStretchFactor(1)\n }\n\n chart.timeScale().fitContent()\n return chart\n }\n\n return { render: renderCapitalChart, version: 1 }\n})()\n";
7
+ export declare const CHART_RUNTIME_SOURCE = "\n(function () {\n 'use strict'\n\n var FALLBACK = {\n '--dsw-alias-bg-base': '#ffffff',\n '--dsw-alias-border-l1': 'rgba(0, 0, 0, 0.10)',\n '--dsw-alias-label-primary': '#1f2328',\n '--dsw-alias-label-secondary': '#6b7280',\n '--dsw-alias-brand-primary': '#2563eb',\n '--dsw-alias-state-error-primary': '#d94a3d',\n '--dsw-alias-state-success-primary': '#2f9e44'\n }\n\n // Hex colors receive alpha; other formats convert to rgba or stay unchanged, never emit invalid CSS.\n function withAlpha(color, alpha) {\n var text = String(color || '').trim()\n var hex = /^#([0-9a-f]{3}|[0-9a-f]{6})$/i.exec(text)\n if (hex) {\n var expanded = hex[1].length === 3\n ? '#' + hex[1].split('').map(function (part) { return part + part }).join('')\n : text\n return expanded + Math.round(alpha * 255).toString(16).padStart(2, '0')\n }\n var rgb = /^rgba?\\(\\s*([^)]*)\\)$/i.exec(text)\n if (rgb) {\n var parts = rgb[1].split(/[,\\s/]+/).filter(Boolean)\n if (parts.length >= 3 && parts.slice(0, 3).every(function (part) { return /^[-+]?\\d*\\.?\\d+%?$/.test(part) })) {\n return 'rgba(' + parts.slice(0, 3).join(', ') + ', ' + alpha + ')'\n }\n }\n return text\n }\n\n // First series uses brand color; later series use a stable palette so one symbol keeps its color.\n var PALETTE = ['#f59e0b', '#10b981', '#8b5cf6', '#0ea5e9', '#ec4899']\n\n function readToken(name) {\n try {\n var value = getComputedStyle(document.documentElement).getPropertyValue(name)\n if (value && value.trim().length > 0) return value.trim()\n } catch (error) {\n /* Non-browser or unavailable styles fall back to defaults. */\n }\n return FALLBACK[name]\n }\n\n function resolveColors() {\n return {\n bg: readToken('--dsw-alias-bg-base'),\n grid: readToken('--dsw-alias-border-l1'),\n text: readToken('--dsw-alias-label-secondary'),\n strong: readToken('--dsw-alias-label-primary'),\n brand: readToken('--dsw-alias-brand-primary'),\n up: readToken('--dsw-alias-state-error-primary'),\n down: readToken('--dsw-alias-state-success-primary')\n }\n }\n\n function showError(container, message) {\n container.textContent = ''\n var box = document.createElement('div')\n box.setAttribute('data-chart-error', '1')\n box.style.cssText = 'padding:12px;border:0.5px solid ' + readToken('--dsw-alias-border-l1') + ';border-radius:8px;color:' + readToken('--dsw-alias-label-secondary') + ';font-size:13px;line-height:1.6'\n box.textContent = message\n container.appendChild(box)\n }\n\n function applyIndexLabels(chart, labels) {\n function format(time) {\n var key = String(time)\n return Object.prototype.hasOwnProperty.call(labels, key) ? labels[key] : key\n }\n // Replace both tick and crosshair formatters for category axes or synthetic dates leak through.\n chart.applyOptions({\n localization: { timeFormatter: format },\n timeScale: { tickMarkFormatter: function (time) { return format(time) } }\n })\n }\n\n var CHART_TIME_ZONE = 'Asia/Shanghai'\n var CHART_DAY_SECONDS = 86400\n var CHART_LOCAL_MIDNIGHT_UTC = 57600\n\n function pad2(value) {\n var text = String(value)\n return text.length < 2 ? '0' + text : text\n }\n\n function timeLabel(time) {\n if (typeof time === 'string') return time\n if (time && typeof time === 'object') {\n var year = time.year\n var month = time.month\n var day = time.day\n if (year !== undefined && month !== undefined && day !== undefined) {\n return String(year) + '-' + pad2(month) + '-' + pad2(day)\n }\n }\n return String(time)\n }\n\n function collectNumericTimes(payload) {\n var times = []\n function add(list) {\n if (!Array.isArray(list)) return\n for (var i = 0; i < list.length; i += 1) {\n var point = list[i]\n if (point && typeof point.time === 'number' && isFinite(point.time)) times.push(point.time)\n }\n }\n if (payload.ohlc) add(payload.ohlc.data)\n if (Array.isArray(payload.series)) {\n for (var i = 0; i < payload.series.length; i += 1) add(payload.series[i] && payload.series[i].data)\n }\n if (payload.volume) add(payload.volume.data)\n return times\n }\n\n // Numeric epochs are true instants; render them in Asia/Shanghai. Business-day strings keep\n // their own labels, so no formatter is applied when every point is a date string.\n function makeTimeAxisFormatter(payload) {\n var times = collectNumericTimes(payload)\n if (times.length === 0) return null\n var min = times[0]\n var max = times[0]\n var daily = true\n for (var i = 0; i < times.length; i += 1) {\n if (times[i] < min) min = times[i]\n if (times[i] > max) max = times[i]\n var mod = ((times[i] % CHART_DAY_SECONDS) + CHART_DAY_SECONDS) % CHART_DAY_SECONDS\n if (mod !== 0 && mod !== CHART_LOCAL_MIDNIGHT_UTC) daily = false\n }\n var span = max - min\n var formatter = new Intl.DateTimeFormat('en-CA', {\n timeZone: CHART_TIME_ZONE,\n year: 'numeric', month: '2-digit', day: '2-digit',\n hour: '2-digit', minute: '2-digit', hourCycle: 'h23'\n })\n function parts(seconds) {\n var values = formatter.formatToParts(new Date(seconds * 1000))\n var out = {}\n for (var j = 0; j < values.length; j += 1) out[values[j].type] = values[j].value\n return out\n }\n function tick(time) {\n if (typeof time !== 'number') return timeLabel(time)\n var p = parts(time)\n if (daily) {\n if (span > 3 * 365 * CHART_DAY_SECONDS) return p.year\n if (span > 120 * CHART_DAY_SECONDS) return p.year + '-' + p.month\n return p.month + '-' + p.day\n }\n if (span > 2 * CHART_DAY_SECONDS) return p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute\n return p.hour + ':' + p.minute\n }\n function full(time) {\n if (typeof time !== 'number') return timeLabel(time)\n var p = parts(time)\n return daily\n ? p.year + '-' + p.month + '-' + p.day\n : p.year + '-' + p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute\n }\n return { tick: tick, full: full }\n }\n\n /** Support both v5 plugin markers and the older series API so markers never disappear silently. */\n function attachMarkers(anchor, markers, LWC) {\n if (typeof LWC.createSeriesMarkers === 'function') {\n LWC.createSeriesMarkers(anchor, markers)\n return true\n }\n if (anchor && typeof anchor.setMarkers === 'function') {\n anchor.setMarkers(markers)\n return true\n }\n return false\n }\n\n function volumePointColor(point, colors) {\n if (point.direction === 'up') return colors.up\n if (point.direction === 'down') return colors.down\n return colors.brand\n }\n\n function seriesColor(index, colors) {\n return index === 0 ? colors.brand : PALETTE[(index - 1) % PALETTE.length]\n }\n\n function appendLegend(container, payload, colors) {\n var items = []\n if (payload.ohlc && payload.ohlc.label) items.push({ label: payload.ohlc.label, color: colors.up })\n if (Array.isArray(payload.series)) {\n payload.series.forEach(function (item, index) {\n if (item && item.label) items.push({ label: item.label, color: seriesColor(index, colors) })\n })\n }\n if (payload.volume && payload.volume.label) items.push({ label: payload.volume.label, color: colors.brand })\n if (items.length === 0) return\n\n var legend = document.createElement('div')\n legend.setAttribute('data-chart-legend', '1')\n legend.style.cssText = 'position:absolute;z-index:2;top:8px;left:8px;right:8px;display:flex;flex-wrap:wrap;gap:4px 10px;align-items:center;pointer-events:none;color:' + colors.text + ';font-size:11px;line-height:16px'\n items.forEach(function (item) {\n var entry = document.createElement('span')\n entry.style.cssText = 'display:inline-flex;align-items:center;gap:4px;padding:1px 5px;border-radius:4px;background:' + withAlpha(colors.bg, 0.8)\n var swatch = document.createElement('span')\n swatch.style.cssText = 'width:7px;height:7px;border-radius:50%;background:' + item.color + ';flex:none'\n var label = document.createElement('span')\n label.textContent = item.label\n entry.appendChild(swatch)\n entry.appendChild(label)\n legend.appendChild(entry)\n })\n container.appendChild(legend)\n }\n\n function renderCapitalChart(container, payload, library) {\n if (!container) throw new Error('renderCapitalChart: container is required')\n if (!payload || typeof payload !== 'object') throw new Error('renderCapitalChart: payload is required')\n // Use the injected library; never fall back to a global copy owned by another plugin.\n var LWC = library\n if (!LWC || typeof LWC.createChart !== 'function') {\n showError(container, '\u56FE\u8868\u5E93\u672A\u52A0\u8F7D\uFF08\u8C03\u7528\u65B9\u672A\u63D0\u4F9B LightweightCharts\uFF09\u3002')\n return null\n }\n\n var colors = resolveColors()\n container.textContent = ''\n if (container.style) container.style.position = 'relative'\n container.setAttribute('data-chart-id', String(payload.chart_id || ''))\n container.setAttribute('data-chart-kind', String(payload.kind || ''))\n\n var chart = LWC.createChart(container, {\n autoSize: true,\n // attributionLogo stays enabled for Apache-2.0 attribution.\n layout: { background: { color: 'transparent' }, textColor: colors.text, fontFamily: 'inherit', attributionLogo: true },\n grid: { vertLines: { color: colors.grid }, horzLines: { color: colors.grid } },\n rightPriceScale: { borderColor: colors.grid, entireTextOnly: true },\n timeScale: { borderColor: colors.grid, rightOffset: 2, barSpacing: 6, minBarSpacing: 0.5 },\n crosshair: { mode: LWC.CrosshairMode ? LWC.CrosshairMode.Normal : 0 },\n localization: { locale: 'zh-CN' }\n })\n\n if (payload.axis === 'index' && payload.labels) applyIndexLabels(chart, payload.labels)\n if (payload.axis === 'time') {\n var timeAxis = makeTimeAxisFormatter(payload)\n if (timeAxis) {\n chart.applyOptions({\n localization: { timeFormatter: timeAxis.full },\n timeScale: { tickMarkFormatter: timeAxis.tick }\n })\n }\n }\n\n var anchor = null\n\n if (payload.ohlc && Array.isArray(payload.ohlc.data) && payload.ohlc.data.length > 0) {\n // kind=bar uses BarSeries; other OHLC kinds use CandlestickSeries with the same data contract.\n var isOhlcBar = payload.kind === 'bar'\n var candles = chart.addSeries(isOhlcBar ? LWC.BarSeries : LWC.CandlestickSeries, {\n upColor: colors.up,\n downColor: colors.down,\n borderUpColor: colors.up,\n borderDownColor: colors.down,\n wickUpColor: colors.up,\n wickDownColor: colors.down\n })\n candles.setData(payload.ohlc.data)\n anchor = candles\n }\n\n if (Array.isArray(payload.series)) {\n for (var index = 0; index < payload.series.length; index += 1) {\n var item = payload.series[index]\n if (!item || !Array.isArray(item.data) || item.data.length === 0) continue\n var color = seriesColor(index, colors)\n var options = { title: item.label || '', priceLineVisible: false, lastValueVisible: true }\n var series = null\n if (item.type === 'area') {\n options.lineColor = color\n options.topColor = withAlpha(color, 0.2)\n options.bottomColor = withAlpha(color, 0.02)\n options.lineWidth = 2\n series = chart.addSeries(LWC.AreaSeries, options)\n } else if (item.type === 'column') {\n // Single-value histograms need HistogramSeries; BarSeries would render an empty chart.\n options.color = color\n options.base = 0\n series = chart.addSeries(LWC.HistogramSeries, options)\n } else {\n options.color = color\n options.lineWidth = 2\n series = chart.addSeries(LWC.LineSeries, options)\n }\n series.setData(item.data)\n if (anchor === null) anchor = series\n }\n }\n\n if (payload.volume && Array.isArray(payload.volume.data) && payload.volume.data.length > 0) {\n var volume = chart.addSeries(LWC.HistogramSeries, {\n title: payload.volume.label || '',\n priceFormat: { type: 'volume' },\n priceLineVisible: false,\n lastValueVisible: false\n }, 1)\n volume.setData(payload.volume.data.map(function (point) {\n return { time: point.time, value: point.value, color: volumePointColor(point, colors) }\n }))\n }\n\n if (anchor === null) {\n showError(container, '\u8FD9\u5F20\u56FE\u6CA1\u6709\u53EF\u6E32\u67D3\u7684\u5E8F\u5217\uFF08payload \u91CC\u65E2\u6CA1\u6709 ohlc \u4E5F\u6CA1\u6709 series\uFF09\u3002')\n return chart\n }\n\n if (Array.isArray(payload.markers) && payload.markers.length > 0) {\n attachMarkers(anchor, payload.markers.map(function (marker) {\n return { time: marker.time, position: marker.position, text: marker.text, color: colors.strong, shape: 'circle' }\n }), LWC)\n }\n\n appendLegend(container, payload, colors)\n\n // Main/volume panes use a 3:1 ratio; a single pane has no second pane object.\n var panes = typeof chart.panes === 'function' ? chart.panes() : []\n if (panes.length > 1) {\n if (typeof panes[0].setStretchFactor === 'function') panes[0].setStretchFactor(3)\n if (typeof panes[1].setStretchFactor === 'function') panes[1].setStretchFactor(1)\n }\n\n chart.timeScale().fitContent()\n return chart\n }\n\n return { render: renderCapitalChart, version: 1 }\n})()\n";
@@ -84,6 +84,92 @@ export const CHART_RUNTIME_SOURCE = `
84
84
  })
85
85
  }
86
86
 
87
+ var CHART_TIME_ZONE = 'Asia/Shanghai'
88
+ var CHART_DAY_SECONDS = 86400
89
+ var CHART_LOCAL_MIDNIGHT_UTC = 57600
90
+
91
+ function pad2(value) {
92
+ var text = String(value)
93
+ return text.length < 2 ? '0' + text : text
94
+ }
95
+
96
+ function timeLabel(time) {
97
+ if (typeof time === 'string') return time
98
+ if (time && typeof time === 'object') {
99
+ var year = time.year
100
+ var month = time.month
101
+ var day = time.day
102
+ if (year !== undefined && month !== undefined && day !== undefined) {
103
+ return String(year) + '-' + pad2(month) + '-' + pad2(day)
104
+ }
105
+ }
106
+ return String(time)
107
+ }
108
+
109
+ function collectNumericTimes(payload) {
110
+ var times = []
111
+ function add(list) {
112
+ if (!Array.isArray(list)) return
113
+ for (var i = 0; i < list.length; i += 1) {
114
+ var point = list[i]
115
+ if (point && typeof point.time === 'number' && isFinite(point.time)) times.push(point.time)
116
+ }
117
+ }
118
+ if (payload.ohlc) add(payload.ohlc.data)
119
+ if (Array.isArray(payload.series)) {
120
+ for (var i = 0; i < payload.series.length; i += 1) add(payload.series[i] && payload.series[i].data)
121
+ }
122
+ if (payload.volume) add(payload.volume.data)
123
+ return times
124
+ }
125
+
126
+ // Numeric epochs are true instants; render them in Asia/Shanghai. Business-day strings keep
127
+ // their own labels, so no formatter is applied when every point is a date string.
128
+ function makeTimeAxisFormatter(payload) {
129
+ var times = collectNumericTimes(payload)
130
+ if (times.length === 0) return null
131
+ var min = times[0]
132
+ var max = times[0]
133
+ var daily = true
134
+ for (var i = 0; i < times.length; i += 1) {
135
+ if (times[i] < min) min = times[i]
136
+ if (times[i] > max) max = times[i]
137
+ var mod = ((times[i] % CHART_DAY_SECONDS) + CHART_DAY_SECONDS) % CHART_DAY_SECONDS
138
+ if (mod !== 0 && mod !== CHART_LOCAL_MIDNIGHT_UTC) daily = false
139
+ }
140
+ var span = max - min
141
+ var formatter = new Intl.DateTimeFormat('en-CA', {
142
+ timeZone: CHART_TIME_ZONE,
143
+ year: 'numeric', month: '2-digit', day: '2-digit',
144
+ hour: '2-digit', minute: '2-digit', hourCycle: 'h23'
145
+ })
146
+ function parts(seconds) {
147
+ var values = formatter.formatToParts(new Date(seconds * 1000))
148
+ var out = {}
149
+ for (var j = 0; j < values.length; j += 1) out[values[j].type] = values[j].value
150
+ return out
151
+ }
152
+ function tick(time) {
153
+ if (typeof time !== 'number') return timeLabel(time)
154
+ var p = parts(time)
155
+ if (daily) {
156
+ if (span > 3 * 365 * CHART_DAY_SECONDS) return p.year
157
+ if (span > 120 * CHART_DAY_SECONDS) return p.year + '-' + p.month
158
+ return p.month + '-' + p.day
159
+ }
160
+ if (span > 2 * CHART_DAY_SECONDS) return p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute
161
+ return p.hour + ':' + p.minute
162
+ }
163
+ function full(time) {
164
+ if (typeof time !== 'number') return timeLabel(time)
165
+ var p = parts(time)
166
+ return daily
167
+ ? p.year + '-' + p.month + '-' + p.day
168
+ : p.year + '-' + p.month + '-' + p.day + ' ' + p.hour + ':' + p.minute
169
+ }
170
+ return { tick: tick, full: full }
171
+ }
172
+
87
173
  /** Support both v5 plugin markers and the older series API so markers never disappear silently. */
88
174
  function attachMarkers(anchor, markers, LWC) {
89
175
  if (typeof LWC.createSeriesMarkers === 'function') {
@@ -163,6 +249,15 @@ export const CHART_RUNTIME_SOURCE = `
163
249
  })
164
250
 
165
251
  if (payload.axis === 'index' && payload.labels) applyIndexLabels(chart, payload.labels)
252
+ if (payload.axis === 'time') {
253
+ var timeAxis = makeTimeAxisFormatter(payload)
254
+ if (timeAxis) {
255
+ chart.applyOptions({
256
+ localization: { timeFormatter: timeAxis.full },
257
+ timeScale: { tickMarkFormatter: timeAxis.tick }
258
+ })
259
+ }
260
+ }
166
261
 
167
262
  var anchor = null
168
263
 
@@ -68,16 +68,16 @@ export declare function extractRows(value: unknown): {
68
68
  shape: string;
69
69
  };
70
70
  interface ParsedTime {
71
- /** Value handed to the library: 'YYYY-MM-DD' business day, or UTC seconds. */
71
+ /** Value handed to the library: 'YYYY-MM-DD' business day, or epoch seconds. */
72
72
  display: string | number;
73
- /** Comparison key (UTC seconds) for sort/filter/alignment. */
73
+ /** Comparison key (epoch seconds); date-only and naive datetime values use Asia/Shanghai. */
74
74
  seconds: number;
75
75
  /** True when the value came from a date-only string; decides business-day rendering. */
76
76
  dateOnly: boolean;
77
77
  }
78
78
  /**
79
- * Deterministic across machines: date-only stays a business-day string (no TZ shift), naive
80
- * datetime is UTC, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
79
+ * Deterministic across machines: date-only is an Asia/Shanghai calendar day, naive datetime is
80
+ * Asia/Shanghai wall clock, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
81
81
  */
82
82
  export declare function parseTimeValue(value: unknown): ParsedTime | undefined;
83
83
  export interface BuildChartPayloadInput {
@@ -17,6 +17,8 @@ const INDEX_EPOCH_SECONDS = Date.UTC(2000, 0, 1) / 1000;
17
17
  const SECONDS_PER_DAY = 86_400;
18
18
  /** Values >= 1e11 are epoch ms (1e11 seconds is year 5138). */
19
19
  const MILLISECONDS_THRESHOLD = 1e11;
20
+ /** Chart convention: data timestamps are Asia/Shanghai wall clock; UTC+8 has no DST for supported dates. */
21
+ const CHART_UTC_OFFSET_MS = 8 * 3_600_000;
20
22
  function isRecord(value) {
21
23
  return Boolean(value) && typeof value === 'object' && !Array.isArray(value);
22
24
  }
@@ -89,9 +91,24 @@ function toNumber(value) {
89
91
  }
90
92
  const DATE_ONLY = /^(\d{4})-(\d{2})-(\d{2})$/;
91
93
  const DATE_TIME = /^(\d{4})-(\d{2})-(\d{2})[ T](\d{2}):(\d{2})(?::(\d{2}))?(?:\.\d+)?(Z|[+-]\d{2}:?\d{2})?$/;
94
+ /** Calendar-valid UTC midnight, or undefined for 2025-02-30 style input. */
95
+ function calendarUtcMs(year, month, day) {
96
+ const utc = Date.UTC(year, month - 1, day);
97
+ const check = new Date(utc);
98
+ if (check.getUTCFullYear() !== year || check.getUTCMonth() + 1 !== month || check.getUTCDate() !== day)
99
+ return undefined;
100
+ return utc;
101
+ }
102
+ /** Asia/Shanghai wall clock to epoch ms; fixed offset matches every supported market date. */
103
+ function shanghaiEpochMs(year, month, day, hour, minute, second) {
104
+ const utc = calendarUtcMs(year, month, day);
105
+ if (utc === undefined)
106
+ return undefined;
107
+ return utc - CHART_UTC_OFFSET_MS + hour * 3_600_000 + minute * 60_000 + second * 1000;
108
+ }
92
109
  /**
93
- * Deterministic across machines: date-only stays a business-day string (no TZ shift), naive
94
- * datetime is UTC, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
110
+ * Deterministic across machines: date-only is an Asia/Shanghai calendar day, naive datetime is
111
+ * Asia/Shanghai wall clock, numbers >= 1e11 are epoch ms, unparseable means the row has no time.
95
112
  */
96
113
  export function parseTimeValue(value) {
97
114
  if (typeof value === 'number') {
@@ -110,16 +127,26 @@ export function parseTimeValue(value) {
110
127
  const year = Number(dateOnly[1]);
111
128
  const month = Number(dateOnly[2]);
112
129
  const day = Number(dateOnly[3]);
113
- const check = new Date(Date.UTC(year, month - 1, day));
114
- if (check.getUTCFullYear() !== year || check.getUTCMonth() + 1 !== month || check.getUTCDate() !== day)
130
+ const epoch = shanghaiEpochMs(year, month, day, 0, 0, 0);
131
+ if (epoch === undefined)
115
132
  return undefined;
116
- return { display: text, seconds: Math.round(check.getTime() / 1000), dateOnly: true };
133
+ return { display: text, seconds: Math.round(epoch / 1000), dateOnly: true };
117
134
  }
118
135
  const dateTime = DATE_TIME.exec(text);
119
136
  if (dateTime) {
120
- const normalized = `${dateTime[1]}-${dateTime[2]}-${dateTime[3]}T${dateTime[4]}:${dateTime[5]}:${dateTime[6] ?? '00'}${dateTime[7] ?? 'Z'}`;
121
- const parsed = Date.parse(normalized);
122
- if (!Number.isFinite(parsed))
137
+ const year = Number(dateTime[1]);
138
+ const month = Number(dateTime[2]);
139
+ const day = Number(dateTime[3]);
140
+ const hour = Number(dateTime[4]);
141
+ const minute = Number(dateTime[5]);
142
+ const second = Number(dateTime[6] ?? '00');
143
+ if (calendarUtcMs(year, month, day) === undefined)
144
+ return undefined;
145
+ // An explicit Z/offset is an instant; a timezone-less value is Asia/Shanghai wall clock.
146
+ const parsed = dateTime[7] === undefined
147
+ ? shanghaiEpochMs(year, month, day, hour, minute, second)
148
+ : Date.parse(`${dateTime[1]}-${dateTime[2]}-${dateTime[3]}T${dateTime[4]}:${dateTime[5]}:${dateTime[6] ?? '00'}${dateTime[7]}`);
149
+ if (parsed === undefined || !Number.isFinite(parsed))
123
150
  return undefined;
124
151
  return { display: Math.round(parsed / 1000), seconds: Math.round(parsed / 1000), dateOnly: false };
125
152
  }
@@ -130,6 +157,13 @@ export function parseTimeValue(value) {
130
157
  return undefined;
131
158
  return { display: Math.round(parsed / 1000), seconds: Math.round(parsed / 1000), dateOnly: false };
132
159
  }
160
+ /** `to` on a date-only bound includes the whole Asia/Shanghai calendar day, not just midnight. */
161
+ function rangeBound(value, edge) {
162
+ const parsed = parseTimeValue(value);
163
+ if (parsed === undefined)
164
+ return undefined;
165
+ return parsed.dateOnly && edge === 'to' ? parsed.seconds + SECONDS_PER_DAY - 1 : parsed.seconds;
166
+ }
133
167
  export function buildChartPayload(input) {
134
168
  const { spec } = input;
135
169
  const warnings = [];
@@ -204,8 +238,8 @@ export function buildChartPayload(input) {
204
238
  if (axis === 'index') {
205
239
  throw new ChartError('chart_spec_invalid', 'spec.range requires a time axis; remove range for category data or provide a parseable time field in spec.x');
206
240
  }
207
- const from = spec.range.from === undefined ? undefined : parseTimeValue(spec.range.from)?.seconds;
208
- const to = spec.range.to === undefined ? undefined : parseTimeValue(spec.range.to)?.seconds;
241
+ const from = spec.range.from === undefined ? undefined : rangeBound(spec.range.from, 'from');
242
+ const to = spec.range.to === undefined ? undefined : rangeBound(spec.range.to, 'to');
209
243
  if (spec.range.from !== undefined && from === undefined)
210
244
  throw new ChartError('chart_spec_invalid', 'spec.range.from is not a recognizable date');
211
245
  if (spec.range.to !== undefined && to === undefined)
@@ -144,6 +144,17 @@ const datasetRefProperties = { dataset_id: { type: 'string' },
144
144
  captured_at: { type: 'integer' },
145
145
  retention_until: { type: 'integer' },
146
146
  params_digest: { type: 'string' },
147
+ time_coverage: {
148
+ type: 'object',
149
+ properties: {
150
+ column: { oneOf: [{ type: 'string' }, { type: 'null' }] },
151
+ from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
152
+ to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
153
+ format: { type: 'string', enum: ['epoch_ms', 'date_string', 'datetime_string', 'none'] },
154
+ },
155
+ required: ['column', 'from', 'to', 'format'],
156
+ additionalProperties: false,
157
+ },
147
158
  };
148
159
  const datasetRefWithAccessSchema = jsonObject({
149
160
  ...datasetRefProperties,
@@ -288,6 +299,17 @@ const describeResultSchema = jsonObject({
288
299
  captured_at: { type: 'integer' },
289
300
  retention_until: { type: 'integer' },
290
301
  params_digest: { type: 'string' },
302
+ time_coverage: {
303
+ type: 'object',
304
+ properties: {
305
+ column: { oneOf: [{ type: 'string' }, { type: 'null' }] },
306
+ from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
307
+ to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
308
+ format: { type: 'string', enum: ['epoch_ms', 'date_string', 'datetime_string', 'none'] },
309
+ },
310
+ required: ['column', 'from', 'to', 'format'],
311
+ additionalProperties: false,
312
+ },
291
313
  query_access: jsonObject({
292
314
  readable: { type: 'boolean' },
293
315
  shape: { type: 'string', enum: ['array', 'envelope_item', 'document', 'none'] },
@@ -171,6 +171,7 @@ export async function describeDataset(input) {
171
171
  captured_at: inspection.captured_at,
172
172
  retention_until: inspection.retention_until,
173
173
  params_digest: inspection.params_digest,
174
+ time_coverage: inspection.time_coverage,
174
175
  query_access: inspection.query_access,
175
176
  profile_id: profile.profile_id,
176
177
  profile_ref: profile.artifact_ref,
@@ -1,3 +1,4 @@
1
+ import { summarizeTimeCoverage } from './time-axis.js';
1
2
  /** Normalize an internal data_key token; data_key is host-only and never model-visible. */
2
3
  export function normalizeKeyToken(value) {
3
4
  return value.replace(/[\\/:]+/g, '.')
@@ -320,6 +321,9 @@ export class DataCollectorHub {
320
321
  throw new Error(`data source ${source.schema.name} returned data incompatible with its output contract`);
321
322
  }
322
323
  const { format, rowCount, rowKey } = shapeOf(output.data, source.schema.rowShape);
324
+ const rows = Array.isArray(output.data)
325
+ ? output.data
326
+ : (rowKey === undefined ? [] : rowsAtPath(output.data, rowKey) ?? []);
323
327
  const ref = await this.store.save({
324
328
  session: item.request.session,
325
329
  capability: item.request.capability,
@@ -331,6 +335,7 @@ export class DataCollectorHub {
331
335
  row_count: rowCount,
332
336
  data: output.data,
333
337
  row_key: rowKey,
338
+ ...(format === 'json_rows' ? { time_coverage: summarizeTimeCoverage(rows) } : {}),
334
339
  });
335
340
  for (const waiter of [...item.waiters])
336
341
  this.settleWaiter(item, waiter, { kind: 'ok', ref });
@@ -1,4 +1,5 @@
1
1
  import { type QueryResult, type QuerySpec } from './query.js';
2
+ import { type DatasetTimeCoverage } from './time-axis.js';
2
3
  /**
3
4
  * Workspace-local Dataset/profile store: immutable artifacts, host-written only. Agents receive
4
5
  * dataset_id / opaque artifact_ref, never real paths; raw rows never enter agent messages.
@@ -38,6 +39,8 @@ export interface DatasetRef {
38
39
  captured_at: number;
39
40
  retention_until: number;
40
41
  params_digest: string;
42
+ /** Host-computed from the stored rows; omitted only by manifests written before this field existed. */
43
+ time_coverage?: DatasetTimeCoverage;
41
44
  }
42
45
  export type DatasetShape = 'array' | 'envelope_item' | 'document' | 'none';
43
46
  export interface DatasetInspection extends DatasetRef {
@@ -293,6 +296,8 @@ export interface SaveDatasetInput {
293
296
  data: unknown;
294
297
  /** Host-only row key for envelope-shaped json_rows data. */
295
298
  row_key?: string;
299
+ /** Computed by the Hub from the same rows it is about to persist. */
300
+ time_coverage?: DatasetTimeCoverage;
296
301
  signal?: AbortSignal;
297
302
  }
298
303
  export interface WriteProfileInput {
@@ -140,6 +140,7 @@ export class WorkspaceDatasetStore {
140
140
  captured_at: capturedAt,
141
141
  retention_until: capturedAt + this.retentionMs,
142
142
  params_digest: input.params_digest,
143
+ ...(input.time_coverage === undefined ? {} : { time_coverage: input.time_coverage }),
143
144
  };
144
145
  const storedManifest = {
145
146
  ...manifest,
@@ -1103,6 +1104,19 @@ function profileQuantile(sorted, probability) {
1103
1104
  return sorted[lower];
1104
1105
  return sorted[lower] + (sorted[upper] - sorted[lower]) * (position - lower);
1105
1106
  }
1107
+ function validTimeCoverage(value) {
1108
+ if (!isRecord(value))
1109
+ return false;
1110
+ if (Object.keys(value).some((key) => !['column', 'from', 'to', 'format'].includes(key)))
1111
+ return false;
1112
+ if (!['epoch_ms', 'date_string', 'datetime_string', 'none'].includes(String(value.format)))
1113
+ return false;
1114
+ if (value.format === 'none')
1115
+ return value.column === null && value.from === null && value.to === null;
1116
+ return typeof value.column === 'string' && value.column.length > 0 && value.column.length <= 128
1117
+ && typeof value.from === 'string' && value.from.length > 0
1118
+ && typeof value.to === 'string' && value.to.length > 0;
1119
+ }
1106
1120
  function validateStoredDatasetManifest(value, datasetId) {
1107
1121
  if (!isRecord(value))
1108
1122
  return undefined;
@@ -1130,6 +1144,8 @@ function validateStoredDatasetManifest(value, datasetId) {
1130
1144
  return undefined;
1131
1145
  if (value.row_key !== undefined && (typeof value.row_key !== 'string' || value.row_key.length === 0 || value.row_key.length > 128))
1132
1146
  return undefined;
1147
+ if (value.time_coverage !== undefined && !validTimeCoverage(value.time_coverage))
1148
+ return undefined;
1133
1149
  const ref = {
1134
1150
  dataset_id: value.dataset_id,
1135
1151
  task_id: value.task_id,
@@ -1143,6 +1159,7 @@ function validateStoredDatasetManifest(value, datasetId) {
1143
1159
  captured_at: value.captured_at,
1144
1160
  retention_until: value.retention_until,
1145
1161
  params_digest: value.params_digest,
1162
+ ...(value.time_coverage === undefined ? {} : { time_coverage: value.time_coverage }),
1146
1163
  };
1147
1164
  return {
1148
1165
  ...ref,
@@ -15,6 +15,21 @@ export interface AxisBounds {
15
15
  data_from: string | null;
16
16
  data_to: string | null;
17
17
  }
18
+ /**
19
+ * Host-computed coverage for a DatasetRef. `format: 'none'` is intentional: a quote source
20
+ * without a time column must not look like it has an as-of time.
21
+ */
22
+ export interface DatasetTimeCoverage {
23
+ column: string | null;
24
+ from: string | null;
25
+ to: string | null;
26
+ format: 'epoch_ms' | 'date_string' | 'datetime_string' | 'none';
27
+ }
28
+ /**
29
+ * Pick one time column by name and return its real min/max. This is a host fact for DatasetRef;
30
+ * it deliberately does not convert quote times across markets (their offsets stay in the string).
31
+ */
32
+ export declare function summarizeTimeCoverage(rows: unknown[]): DatasetTimeCoverage;
18
33
  /**
19
34
  * Translate a date-like filter value into comparable bounds. Dates mean whole days: `=` expands
20
35
  * to >=/<=, while `>`, `<`, `in` and `!=` fail loudly instead of silently shifting a day.
@@ -126,6 +126,79 @@ export function probeTimeAxis(column, values) {
126
126
  return { column, value_format: 'date_string', aligned_to: 'calendar_day' };
127
127
  return { column, value_format: 'unknown' };
128
128
  }
129
+ const TIME_COLUMN_PRIORITY = [
130
+ 'quote_time', 'event_time_beijing', 'datetime', 'report_date', 'trade_date',
131
+ 'stat_date', 'quote_date', 'collect_date', 'date', 'time',
132
+ 'quote_time_ms', 'event_time_ms', 'report_date_ms', 'trade_date_ms',
133
+ 'stat_date_ms', 'quote_date_ms', 'collect_date_ms', 'date_ms', 'time_ms',
134
+ ];
135
+ const COVERAGE_DATE_RE = /^\d{4}-\d{2}-\d{2}$/;
136
+ const COVERAGE_DATETIME_RE = /^\d{4}-\d{2}-\d{2}[ T]\d{2}:\d{2}(?::\d{2}(?:\.\d{1,9})?)?(?:Z|[+-]\d{2}:\d{2})?$/;
137
+ function coverageOfColumn(column, rows) {
138
+ const values = [];
139
+ for (const row of rows) {
140
+ const cell = cellOf(row, column);
141
+ if (!cell.present || cell.value === null || cell.value === undefined)
142
+ continue;
143
+ values.push(cell.value);
144
+ }
145
+ if (values.length === 0)
146
+ return undefined;
147
+ const numbers = values.filter((value) => typeof value === 'number' && Number.isFinite(value));
148
+ if (numbers.length === values.length) {
149
+ let min = Number.POSITIVE_INFINITY;
150
+ let max = Number.NEGATIVE_INFINITY;
151
+ for (const value of numbers) {
152
+ if (!Number.isSafeInteger(value) || value < MIN_EPOCH_MS || value > 8_640_000_000_000_000)
153
+ return undefined;
154
+ if (value < min)
155
+ min = value;
156
+ if (value > max)
157
+ max = value;
158
+ }
159
+ const axis = probeTimeAxis(column, numbers);
160
+ const zone = axis.time_zone ?? 'UTC';
161
+ const text = (value) => axis.aligned_to === 'local_midnight'
162
+ ? isoDateFromEpoch(value, zone)
163
+ : new Date(value).toISOString();
164
+ return { column, from: text(min), to: text(max), format: 'epoch_ms' };
165
+ }
166
+ const strings = values.filter((value) => typeof value === 'string');
167
+ if (strings.length !== values.length)
168
+ return undefined;
169
+ const trimmed = strings.map((value) => value.trim());
170
+ if (trimmed.every((value) => COVERAGE_DATE_RE.test(value))) {
171
+ const sorted = [...trimmed].sort();
172
+ return { column, from: sorted[0], to: sorted[sorted.length - 1], format: 'date_string' };
173
+ }
174
+ if (trimmed.every((value) => COVERAGE_DATETIME_RE.test(value))) {
175
+ const sorted = [...trimmed].map((value) => value.replace('T', ' ')).sort();
176
+ return { column, from: sorted[0], to: sorted[sorted.length - 1], format: 'datetime_string' };
177
+ }
178
+ return undefined;
179
+ }
180
+ /**
181
+ * Pick one time column by name and return its real min/max. This is a host fact for DatasetRef;
182
+ * it deliberately does not convert quote times across markets (their offsets stay in the string).
183
+ */
184
+ export function summarizeTimeCoverage(rows) {
185
+ if (rows.length === 0)
186
+ return { column: null, from: null, to: null, format: 'none' };
187
+ const first = rows.find((row) => isRecordLike(row));
188
+ if (!isRecordLike(first))
189
+ return { column: null, from: null, to: null, format: 'none' };
190
+ const keys = Object.keys(first);
191
+ const ordered = [
192
+ ...TIME_COLUMN_PRIORITY.filter((column) => keys.includes(column)),
193
+ ...keys.filter((column) => !TIME_COLUMN_PRIORITY.includes(column) && /(^|_)(date|time)(_|$)|_ms$/i.test(column)),
194
+ ];
195
+ for (const column of ordered) {
196
+ const coverage = coverageOfColumn(column, rows);
197
+ if (coverage !== undefined)
198
+ return coverage;
199
+ }
200
+ return { column: null, from: null, to: null, format: 'none' };
201
+ }
129
202
  /**
130
203
  * Translate a date-like filter value into comparable bounds. Dates mean whole days: `=` expands
131
204
  * to >=/<=, while `>`, `<`, `in` and `!=` fail loudly instead of silently shifting a day.
@@ -29,4 +29,4 @@ export interface DataCollectorDiagnostics {
29
29
  }
30
30
  export declare function registerDataCollectorTools(ctx: Context, hub: DataCollectorHub, diagnostics?: DataCollectorDiagnostics): void;
31
31
  /** Model-visible DatasetRef field whitelist (tests and host assertions). */
32
- export declare const DATASET_REF_FIELDS: readonly ["dataset_id", "task_id", "session_id", "artifact_ref", "format", "capability", "source_label", "schema", "row_count", "captured_at", "retention_until", "params_digest"];
32
+ export declare const DATASET_REF_FIELDS: readonly ["dataset_id", "task_id", "session_id", "artifact_ref", "format", "capability", "source_label", "schema", "row_count", "captured_at", "retention_until", "params_digest", "time_coverage"];
@@ -42,6 +42,17 @@ const datasetRefSchema = () => jsonObject({
42
42
  captured_at: { type: 'integer' },
43
43
  retention_until: { type: 'integer' },
44
44
  params_digest: { type: 'string' },
45
+ time_coverage: {
46
+ type: 'object',
47
+ properties: {
48
+ column: { oneOf: [{ type: 'string' }, { type: 'null' }] },
49
+ from: { oneOf: [{ type: 'string' }, { type: 'null' }] },
50
+ to: { oneOf: [{ type: 'string' }, { type: 'null' }] },
51
+ format: { type: 'string', enum: ['epoch_ms', 'date_string', 'datetime_string', 'none'] },
52
+ },
53
+ required: ['column', 'from', 'to', 'format'],
54
+ additionalProperties: false,
55
+ },
45
56
  }, ['dataset_id', 'task_id', 'session_id', 'artifact_ref', 'format', 'capability', 'source_label', 'schema', 'row_count', 'captured_at', 'retention_until', 'params_digest']);
46
57
  function render(_args, value) {
47
58
  // ToolOutputDefinition.render(args, value): the first parameter is the input, the second the output.
@@ -215,5 +226,5 @@ export function registerDataCollectorTools(ctx, hub, diagnostics) {
215
226
  export const DATASET_REF_FIELDS = [
216
227
  'dataset_id', 'task_id', 'session_id', 'artifact_ref', 'format',
217
228
  'capability', 'source_label', 'schema', 'row_count', 'captured_at',
218
- 'retention_until', 'params_digest',
229
+ 'retention_until', 'params_digest', 'time_coverage',
219
230
  ];
package/lib/index.d.ts CHANGED
@@ -20,6 +20,7 @@ export interface Config {
20
20
  maxSteps?: number;
21
21
  maxToolCalls?: number;
22
22
  inAppNotifications?: boolean;
23
+ defaultWorkspaceId?: string;
23
24
  };
24
25
  retriever?: RetrieverConfig;
25
26
  }
@@ -40,6 +41,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
40
41
  maxSteps: z<number, number, "defined">;
41
42
  maxToolCalls: z<number, number, "defined">;
42
43
  inAppNotifications: z<boolean, boolean, "defined">;
44
+ defaultWorkspaceId: z<string, string, "defined">;
43
45
  }>>, Schemastery.ObjectT<NoInfer<{
44
46
  tickMs: z<number, number, "defined">;
45
47
  runTimeoutMinutes: z<number, number, "defined">;
@@ -47,6 +49,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
47
49
  maxSteps: z<number, number, "defined">;
48
50
  maxToolCalls: z<number, number, "defined">;
49
51
  inAppNotifications: z<boolean, boolean, "defined">;
52
+ defaultWorkspaceId: z<string, string, "defined">;
50
53
  }>>, "defined">;
51
54
  retriever: z<Schemastery.ObjectS<NoInfer<{
52
55
  baseURL: z<string, string, "defined">;
@@ -142,6 +145,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
142
145
  maxSteps: z<number, number, "defined">;
143
146
  maxToolCalls: z<number, number, "defined">;
144
147
  inAppNotifications: z<boolean, boolean, "defined">;
148
+ defaultWorkspaceId: z<string, string, "defined">;
145
149
  }>>, Schemastery.ObjectT<NoInfer<{
146
150
  tickMs: z<number, number, "defined">;
147
151
  runTimeoutMinutes: z<number, number, "defined">;
@@ -149,6 +153,7 @@ export declare const Config: z<Schemastery.ObjectS<NoInfer<{
149
153
  maxSteps: z<number, number, "defined">;
150
154
  maxToolCalls: z<number, number, "defined">;
151
155
  inAppNotifications: z<boolean, boolean, "defined">;
156
+ defaultWorkspaceId: z<string, string, "defined">;
152
157
  }>>, "defined">;
153
158
  retriever: z<Schemastery.ObjectS<NoInfer<{
154
159
  baseURL: z<string, string, "defined">;