@v587d/capital-generation 2.8.3 → 2.9.1

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 (169) hide show
  1. package/CHANGELOG.md +81 -1
  2. package/README.md +40 -19
  3. package/chart-ui/README.md +5 -4
  4. package/chart-ui/client.src.cjs +7 -16
  5. package/chart-ui/index.js +12 -33
  6. package/chart-ui/package.json +2 -2
  7. package/config/client.js +18 -39
  8. package/config/client.src.cjs +79 -217
  9. package/config/index.js +22 -72
  10. package/config/package.json +2 -2
  11. package/cordis.patch.yml +46 -51
  12. package/lib/agents/bash-guard.d.ts +9 -55
  13. package/lib/agents/bash-guard.js +27 -67
  14. package/lib/agents/preset-roster.d.ts +22 -0
  15. package/lib/agents/preset-roster.js +87 -0
  16. package/lib/agents/role-tool-policy.d.ts +46 -0
  17. package/lib/agents/role-tool-policy.js +205 -0
  18. package/lib/agents/root-tool-policy.d.ts +28 -124
  19. package/lib/agents/root-tool-policy.js +164 -237
  20. package/lib/agents/selected-skills-provider.d.ts +2 -6
  21. package/lib/agents/selected-skills-provider.js +27 -84
  22. package/lib/capital-settings.d.ts +27 -0
  23. package/lib/capital-settings.js +23 -0
  24. package/lib/chart/artifact-ref.d.ts +13 -8
  25. package/lib/chart/artifact-ref.js +27 -10
  26. package/lib/chart/downsample.d.ts +2 -5
  27. package/lib/chart/downsample.js +3 -8
  28. package/lib/chart/errors.d.ts +3 -7
  29. package/lib/chart/errors.js +1 -2
  30. package/lib/chart/event-context.d.ts +8 -0
  31. package/lib/chart/event-context.js +39 -0
  32. package/lib/chart/events.d.ts +21 -78
  33. package/lib/chart/events.js +43 -189
  34. package/lib/chart/html.d.ts +2 -5
  35. package/lib/chart/html.js +8 -15
  36. package/lib/chart/present-tool.d.ts +28 -0
  37. package/lib/chart/present-tool.js +128 -0
  38. package/lib/chart/runtime.d.ts +5 -18
  39. package/lib/chart/runtime.js +14 -34
  40. package/lib/chart/series.d.ts +10 -17
  41. package/lib/chart/series.js +22 -47
  42. package/lib/chart/source-token.d.ts +5 -13
  43. package/lib/chart/source-token.js +18 -17
  44. package/lib/chart/spec.d.ts +6 -12
  45. package/lib/chart/spec.js +7 -31
  46. package/lib/chart/tool.d.ts +7 -29
  47. package/lib/chart/tool.js +15 -25
  48. package/lib/chart-render-entry.d.ts +10 -0
  49. package/lib/chart-render-entry.js +14 -0
  50. package/lib/chart-source-entry.d.ts +13 -0
  51. package/lib/chart-source-entry.js +20 -0
  52. package/lib/collect-entry.d.ts +9 -0
  53. package/lib/collect-entry.js +10 -0
  54. package/lib/collector-runtime.d.ts +22 -0
  55. package/lib/collector-runtime.js +97 -0
  56. package/lib/data-collector/args.d.ts +6 -24
  57. package/lib/data-collector/args.js +6 -24
  58. package/lib/data-collector/dataset-tools.d.ts +7 -1
  59. package/lib/data-collector/dataset-tools.js +27 -56
  60. package/lib/data-collector/describe.d.ts +8 -33
  61. package/lib/data-collector/describe.js +17 -70
  62. package/lib/data-collector/hub.d.ts +27 -85
  63. package/lib/data-collector/hub.js +19 -85
  64. package/lib/data-collector/query-time.d.ts +7 -26
  65. package/lib/data-collector/query-time.js +8 -27
  66. package/lib/data-collector/store.d.ts +41 -117
  67. package/lib/data-collector/store.js +32 -111
  68. package/lib/data-collector/time-axis.d.ts +16 -76
  69. package/lib/data-collector/time-axis.js +25 -111
  70. package/lib/data-collector/tools.d.ts +6 -6
  71. package/lib/data-collector/tools.js +34 -43
  72. package/lib/dataset-read-entry.d.ts +9 -0
  73. package/lib/dataset-read-entry.js +7 -0
  74. package/lib/index.d.ts +7 -117
  75. package/lib/index.js +43 -422
  76. package/lib/net/eastmoney-client.d.ts +13 -65
  77. package/lib/net/eastmoney-client.js +12 -53
  78. package/lib/net/throttle.d.ts +2 -13
  79. package/lib/net/throttle.js +3 -14
  80. package/lib/ocr/client.d.ts +12 -21
  81. package/lib/ocr/client.js +23 -40
  82. package/lib/ocr/markdown.d.ts +20 -26
  83. package/lib/ocr/markdown.js +23 -36
  84. package/lib/ocr/store.d.ts +14 -47
  85. package/lib/ocr/store.js +15 -45
  86. package/lib/ocr-entry.d.ts +9 -0
  87. package/lib/ocr-entry.js +10 -0
  88. package/lib/policy-entry.d.ts +17 -0
  89. package/lib/policy-entry.js +22 -0
  90. package/lib/present-entry.d.ts +9 -0
  91. package/lib/present-entry.js +13 -0
  92. package/lib/preset-claim.d.ts +22 -0
  93. package/lib/preset-claim.js +24 -0
  94. package/lib/preset-ids.d.ts +38 -0
  95. package/lib/preset-ids.js +46 -0
  96. package/lib/retriever-config.d.ts +37 -0
  97. package/lib/retriever-config.js +36 -0
  98. package/lib/retriever-runtime.d.ts +31 -0
  99. package/lib/retriever-runtime.js +82 -0
  100. package/lib/root-extras-entry.d.ts +17 -0
  101. package/lib/root-extras-entry.js +25 -0
  102. package/lib/runtime-entry.d.ts +15 -0
  103. package/lib/runtime-entry.js +5 -0
  104. package/lib/runtime.d.ts +21 -0
  105. package/lib/runtime.js +33 -0
  106. package/lib/search-entry.d.ts +9 -0
  107. package/lib/search-entry.js +9 -0
  108. package/lib/sources/eastmoney-http.js +20 -68
  109. package/lib/sources/ftshare-rest.d.ts +6 -34
  110. package/lib/sources/ftshare-rest.js +84 -292
  111. package/lib/sources/fuyao-core.d.ts +8 -18
  112. package/lib/sources/fuyao-core.js +10 -21
  113. package/lib/sources/fuyao-rest.d.ts +2 -6
  114. package/lib/sources/fuyao-rest.js +23 -102
  115. package/lib/sources/security-code.d.ts +6 -12
  116. package/lib/sources/security-code.js +6 -12
  117. package/lib/sources/security-identity.d.ts +1 -4
  118. package/lib/sources/security-identity.js +3 -8
  119. package/lib/sources/tencent-http.js +14 -28
  120. package/lib/sources/tencent-public-core.d.ts +8 -60
  121. package/lib/sources/tencent-public-core.js +12 -77
  122. package/lib/sources/wind-mcp.d.ts +0 -5
  123. package/lib/sources/wind-mcp.js +23 -78
  124. package/lib/sources-entry.d.ts +9 -0
  125. package/lib/sources-entry.js +9 -0
  126. package/lib/time/tools.d.ts +8 -31
  127. package/lib/time/tools.js +30 -66
  128. package/lib/time-entry.d.ts +9 -0
  129. package/lib/time-entry.js +7 -0
  130. package/lib/tool-exec.d.ts +45 -11
  131. package/lib/tool-exec.js +59 -6
  132. package/lib/user-customization.d.ts +14 -0
  133. package/lib/user-customization.js +47 -0
  134. package/lib/watchlist/tools.d.ts +3 -13
  135. package/lib/watchlist/tools.js +10 -30
  136. package/lib/watchlist-entry.d.ts +9 -0
  137. package/lib/watchlist-entry.js +6 -0
  138. package/lib/web-retriever/engines.d.ts +6 -11
  139. package/lib/web-retriever/engines.js +9 -14
  140. package/lib/web-retriever/html-markdown.d.ts +14 -34
  141. package/lib/web-retriever/html-markdown.js +39 -75
  142. package/lib/web-retriever/local-fetch.d.ts +22 -58
  143. package/lib/web-retriever/local-fetch.js +33 -77
  144. package/lib/web-retriever/retriever.d.ts +6 -18
  145. package/lib/web-retriever/retriever.js +7 -7
  146. package/lib/web-retriever/sources.d.ts +37 -124
  147. package/lib/web-retriever/sources.js +64 -196
  148. package/lib/web-retriever/tools.d.ts +43 -27
  149. package/lib/web-retriever/tools.js +99 -137
  150. package/lib/web-retriever/wind-client.d.ts +14 -31
  151. package/lib/web-retriever/wind-client.js +16 -32
  152. package/locale/zh.json +1 -1
  153. package/package.json +72 -7
  154. package/preset/capital-generation/README.md +87 -53
  155. package/preset/capital-generation/agent.patch.yml +532 -337
  156. package/preset/capital-generation/skills/capital-chart-protocol/SKILL.md +6 -4
  157. package/preset/capital-generation/skills/capital-data-profile/SKILL.md +2 -2
  158. package/preset/capital-generation/skills/capital-data-protocol/SKILL.md +1 -1
  159. package/preset/capital-generation/skills/capital-equity-research/SKILL.md +4 -2
  160. package/preset/capital-generation/skills/capital-event-impact/SKILL.md +1 -1
  161. package/preset/capital-generation/skills/capital-orchestration/SKILL.md +30 -33
  162. package/preset/capital-generation/skills/capital-portfolio-review/SKILL.md +1 -1
  163. package/preset/capital-generation/skills/capital-visualization-protocol/SKILL.md +14 -16
  164. package/preset/capital-generation/skills/capital-web-evidence/SKILL.md +1 -1
  165. package/watchlist/client.js +15 -148
  166. package/watchlist/client.src.cjs +148 -662
  167. package/watchlist/holding-rules.js +11 -38
  168. package/watchlist/index.js +127 -397
  169. package/watchlist/package.json +2 -2
@@ -1,35 +1,19 @@
1
1
  import type { Context } from '@deepseek-ai/cordis';
2
2
  import type { DataTimeContract } from '../time/tools.js';
3
3
  import type { DatasetRef, SaveDatasetInput, SessionLike } from './store.js';
4
- /**
5
- * 规范化内部 data_key 片段:斜杠/冒号等路径分隔符一律映射为点,其余非常规
6
- * 字符折叠为点、合并连续点、去掉首尾点。data_key 只是宿主内部路由/审计身份,
7
- * 不出现在模型可见协议、人设或工具输出中。
8
- */
4
+ /** Normalize an internal data_key token; data_key is host-only and never model-visible. */
9
5
  export declare function normalizeKeyToken(value: string): string;
10
- /**
11
- * 生成内部 data_key:provider.kind.resource(例如
12
- * buildDataKey('fuyao', 'api', '/api/a-share/prices/snapshot')
13
- * => 'fuyao.api.api.a-share.prices.snapshot')。
14
- */
15
6
  export declare function buildDataKey(provider: string, kind: string, resource: string): string;
16
- /**
17
- * 数据请求:capability 是模型可见的短能力名(全局唯一),session 由工具层以
18
- * exec.agent.session 注入,不接受模型传参。内部 data_key 不出现在请求协议里。
19
- */
7
+ /** capability is the model-visible unique key; session is injected by the tool layer, not supplied by the model. */
20
8
  export interface DataRequest {
21
9
  capability: string;
22
10
  params: Record<string, unknown>;
23
- /** force_refresh=true 绕过已验证 manifest 并生成新的不可变 Dataset。 */
11
+ /** force_refresh bypasses manifest reuse and creates a new immutable Dataset. */
24
12
  force_refresh?: boolean;
25
- /** 本次用户任务关联 id(可复用);只用于追溯,不要求模型理解。 */
26
13
  task_id?: string;
27
14
  session: SessionLike;
28
15
  }
29
- /**
30
- * 数据源契约:capability 为唯一注册键与模型可见名;name/source/data_key
31
- * 均为宿主内部字段(日志、路由、审计),不得进入工具 schema、输出或人设。
32
- */
16
+ /** capability is the unique registration key; name/source/data_key stay host-internal (logs, routing, audit). */
33
17
  export interface SchemaDescriptor {
34
18
  capability: string;
35
19
  name: string;
@@ -37,34 +21,29 @@ export interface SchemaDescriptor {
37
21
  data_key: string;
38
22
  source_label?: string;
39
23
  paginated?: boolean;
40
- /** 可复用 Dataset 的最长采集年龄;未声明时仅受 Dataset 保留期约束。宿主内部字段。 */
24
+ /** Longest reusable Dataset age; defaults to the retention window. Host-only. */
41
25
  cacheMaxAgeMs?: number;
42
- /**
43
- * 行数组位置的显式声明。龙虎榜这类响应的行数组不在 `item` 下(`stock_items`),
44
- * 只靠形状推断会把整份数据判成「不可读的文档」。谁产出数据谁声明行在哪里,
45
- * 存储层不再猜。
46
- */
26
+ /** Where the row array lives; without it, envelopes such as `stock_items` look unreadable. */
47
27
  rowShape?: RowShapeHint;
48
- /** 一行摘要:只用于能力目录(选能力够用),控制在 ~50 字以内。 */
28
+ /** One-line catalog summary (~50 chars max). */
49
29
  summary?: string;
50
- /** 完整说明:只在 describe_capability 详情里返回(单位、null 语义、时间口径、分页口径)。 */
30
+ /** Full detail for describe_capability only (units, null semantics, time/pagination rules). */
51
31
  description?: string;
52
32
  input_schema: object;
53
- /** 机器可读时间参数契约,由时间工具与数据源共同使用。 */
33
+ /** Machine-readable time-parameter contract shared with the time tools. */
54
34
  time_contract?: DataTimeContract;
55
35
  output_schema?: object;
56
36
  }
57
- /** 能力目录项:只带"选能力"所需的信息,体积必须远小于工具结果剪枝阈值。 */
37
+ /** Catalog entry: just enough to pick a capability. */
58
38
  export interface CapabilitySummary {
59
39
  capability: string;
60
40
  summary: string;
61
41
  paginated: boolean;
62
42
  }
63
- /** 单个能力的完整契约:参数与输出字段字典,调用前必读。 */
43
+ /** Full contract for one capability; must be read before calling. */
64
44
  export interface CapabilityDetail extends CapabilitySummary {
65
45
  description: string;
66
46
  input_schema: object;
67
- /** `output_schema` 的模型可见投影,见 renderOutputFields;声明不出字段时为 null。 */
68
47
  output_fields: string | null;
69
48
  }
70
49
  export interface DataSource {
@@ -73,22 +52,16 @@ export interface DataSource {
73
52
  data: unknown;
74
53
  schema?: object | null;
75
54
  }>;
76
- /** Optional source-specific output guard; false rejects the result before persistence. */
55
+ /** Optional source guard; false rejects the result before persistence. */
77
56
  validateOutput?: (data: unknown) => boolean;
78
57
  /**
79
- * Optional parameter canonicalization. The Hub calls it BEFORE computing the
80
- * in-flight merge key and params_digest, so requests that differ only in
81
- * case/whitespace/list order/default values share one Dataset instead of
82
- * writing several. It must be idempotent (execute calls it again) and may
83
- * throw to reject invalid parameters before anything is queued. Sources that
84
- * omit it keep their previous behaviour exactly.
58
+ * Optional canonicalization; the Hub calls it before merge key and params_digest, so equivalent
59
+ * requests share one Dataset. Must be idempotent (execute calls it again) and may throw.
85
60
  */
86
61
  normalizeParams?: (params: Record<string, unknown>) => Record<string, unknown>;
87
62
  }
88
- /** Dataset 落盘器(Hub 的唯一成功出口)。 */
89
63
  export interface DatasetStoreLike {
90
64
  save(input: SaveDatasetInput): Promise<DatasetRef>;
91
- /** Optional for compatibility with lightweight test stores; production store implements manifest lookup. */
92
65
  findLatest?(input: {
93
66
  session: SessionLike;
94
67
  capability: string;
@@ -96,35 +69,29 @@ export interface DatasetStoreLike {
96
69
  signal?: AbortSignal;
97
70
  }): Promise<DatasetRef | undefined>;
98
71
  }
99
- /** 数据源声明的行数组位置;缺省按 `item` 推断。 */
100
72
  export interface RowShapeHint {
101
- /** 行数组所在的顶层键名(默认 `item`)。 */
73
+ /** Top-level key holding rows (default `item`). */
102
74
  rowKey?: string;
103
- /** 可选的多个行数组路径,支持 `[]` 展开并合并为一组 rows。 */
75
+ /** Multiple row paths; `[]` expands and merges them into one row set. */
104
76
  rowKeys?: string[];
105
- /** 顶层本身就是行数组。 */
77
+ /** The top level is itself the row array. */
106
78
  rootArray?: boolean;
107
79
  }
108
80
  export interface DataCollectorHubOptions {
109
- /** 必需:宿主侧 Dataset store;缺失时 request 直接失败。 */
81
+ /** Required Dataset store; request fails without it. */
110
82
  store: DatasetStoreLike;
111
- /** 队列最大长度;默认 50。 */
83
+ /** Queue length (default 50). */
112
84
  maxQueueLength?: number;
113
- /** 传输层执行超时;默认 30_000 ms。只是请求超时,不是数据生命周期。 */
85
+ /** Transport timeout only (30s default), not data lifetime. */
114
86
  requestTimeoutMs?: number;
115
- /** 时钟注入(测试用);默认 Date.now。 */
116
87
  now?: () => number;
117
88
  }
118
89
  /**
119
- * Phase 1 数据执行器:FIFO 串行执行外部数据源,成功后由宿主 store 立即
120
- * 原子落盘为不可变 Dataset,只回传 DatasetRef。
121
- *
122
- * 明确不做:长期 raw data 内存缓存、按 data_key 的查询、source_preference 路由。
123
- * 已验证 DatasetRef 的复用由 workspace manifest 提供,内存只保留 in-flight 队列。
90
+ * FIFO executor: success is persisted immediately as an immutable Dataset and only a DatasetRef is
91
+ * returned. In-flight queue is the only in-memory raw data; reuse comes from workspace manifests.
124
92
  */
125
93
  export declare class DataCollectorHub {
126
94
  private readonly queue;
127
- /** 正在执行的队列项(执行期间不在 queue 中,但仍参与合并与容量判断)。 */
128
95
  private active;
129
96
  private readonly sources;
130
97
  private readonly cacheLookups;
@@ -136,46 +103,21 @@ export declare class DataCollectorHub {
136
103
  constructor(options: DataCollectorHubOptions);
137
104
  registerSource(source: DataSource): () => void;
138
105
  /**
139
- * 入队并阻塞等待完成:先按 capability+params_digest 查找当前 session 下仍有效的
140
- * manifest;force_refresh=true 或没有可复用 Dataset 时才进入数据源队列。相同的
141
- * in-flight 请求仍共享一次执行,成功后立即由宿主 store 原子落盘。
142
- *
143
- * 参数规范化(source 可选)发生在这里:merge key 与 digest 都用规范化后的参数,
144
- * 因此「大小写/空白/重复项不同、语义相同」的请求命中同一个 Dataset。未知
145
- * capability 不做处理,仍由执行阶段抛出统一的 no data source 错误。
106
+ * Enqueue and wait: reuse a valid manifest unless force_refresh; identical in-flight requests share
107
+ * one execution. Params are normalized before merge key and digest.
146
108
  */
147
109
  request(request: DataRequest, options?: {
148
110
  signal?: AbortSignal;
149
111
  }): Promise<DatasetRef>;
150
- /** 应用 source 的可选参数规范化;未声明或 capability 未注册时原样返回。 */
151
112
  private normalizeRequest;
152
- /** 挂一个等待者并返回其结算承诺;signal 触发时摘除等待者并拒绝(不影响共享执行)。 */
153
113
  private awaitSettlement;
154
- /** 结算一个等待者:无论成败都摘除监听,防止重复结算。 */
155
114
  private settleWaiter;
156
- /**
157
- * 能力目录(模型可见的最外层发现入口):只含 capability、一行摘要与是否分页。
158
- *
159
- * 目录刻意不带 input_schema / output_schema —— 全量 schema 一度达 23KB,
160
- * 会被工具结果剪枝器截掉中间部分,导致排在中间的能力在发现阶段不可见。
161
- * 目录必须一次性全部返回(体积预算由测试守住),详情走 describeCapability。
162
- * 不含任何内部路由字段(name/source/data_key/source_label)。
163
- */
115
+ /** Catalog: capability, one-line summary, paginated only. Full schemas would be pruned; details come from describeCapability. */
164
116
  listCapabilities(): CapabilitySummary[];
165
- /** 当前已注册的能力名(用于未知名字的引导与诊断)。 */
166
117
  capabilityNames(): string[];
167
- /**
168
- * 能力目录的模型可见形态:一行一条 `capability|summary|paginated`(1=分页、0=不分页)。
169
- * 同等信息量下 JSON 数组要 5530 字符(69 条实测,其中 58% 是键名与标点),行编码 2435、
170
- * 均摊从 80 降到 35 字符——目录余量从 +7 条变成 +105 条。信息量没变、依然一次性全量返回,
171
- * §1.1 那条"不分页、不裁剪"的硬约束原样成立。结构化形态仍由 listCapabilities 提供(测试与内部消费)。
172
- */
118
+ /** Model-visible catalog: one `capability|summary|paginated` line each; JSON costs about twice the tokens. */
173
119
  capabilityDirectory(): string;
174
- /**
175
- * 单个能力的完整契约;名字未注册时返回 undefined,由工具层分类成可引导的错误。
176
- * 返回值只含模型可见字段,是新建的普通对象,不回传内部 live data。
177
- * `output_fields` 是 `output_schema` 的投影:详情体积按列数增长会被宽表打穿,见 renderOutputFields。
178
- */
120
+ /** Single capability contract; undefined for unknown names (the tool layer classifies it). */
179
121
  describeCapability(capability: string): CapabilityDetail | undefined;
180
122
  private processNext;
181
123
  }
@@ -1,23 +1,13 @@
1
- /**
2
- * 规范化内部 data_key 片段:斜杠/冒号等路径分隔符一律映射为点,其余非常规
3
- * 字符折叠为点、合并连续点、去掉首尾点。data_key 只是宿主内部路由/审计身份,
4
- * 不出现在模型可见协议、人设或工具输出中。
5
- */
1
+ /** Normalize an internal data_key token; data_key is host-only and never model-visible. */
6
2
  export function normalizeKeyToken(value) {
7
3
  return value.replace(/[\\/:]+/g, '.')
8
4
  .replace(/[^A-Za-z0-9._-]+/g, '.')
9
5
  .replace(/\.{2,}/g, '.')
10
6
  .replace(/^\.+|\.+$/g, '');
11
7
  }
12
- /**
13
- * 生成内部 data_key:provider.kind.resource(例如
14
- * buildDataKey('fuyao', 'api', '/api/a-share/prices/snapshot')
15
- * => 'fuyao.api.api.a-share.prices.snapshot')。
16
- */
17
8
  export function buildDataKey(provider, kind, resource) {
18
9
  return [provider, kind, resource].map(normalizeKeyToken).filter(Boolean).join('.');
19
10
  }
20
- /** 目录项摘要的兜底:描述缺失或没有 summary 时取首句并截断。 */
21
11
  function summarize(source) {
22
12
  if (typeof source.summary === 'string' && source.summary.trim().length > 0)
23
13
  return source.summary.trim();
@@ -27,10 +17,7 @@ function summarize(source) {
27
17
  const firstSentence = description.split(/[。;;.]/)[0]?.trim() ?? description;
28
18
  return firstSentence.length > 80 ? `${firstSentence.slice(0, 79)}…` : firstSentence;
29
19
  }
30
- /**
31
- * 目录字段的转义:`|` 与换行是行编码的分隔符,撞上去的后果是**一条能力被读成两条**
32
- * (错了不报错、只是话说错那一族),所以由生产者转义而不是指望摘要里恰好没有。
33
- */
20
+ /** Escape `|` and newlines: they delimit catalog lines, and one unescaped splits a capability in two. */
34
21
  function encodeDirectoryField(value) {
35
22
  return value
36
23
  .replace(/\\/g, '\\\\')
@@ -42,14 +29,13 @@ function encodeDirectoryField(value) {
42
29
  function isPlainRecord(value) {
43
30
  return typeof value === 'object' && value !== null && !Array.isArray(value);
44
31
  }
45
- /** 说明段里不许有换行:一个字段变成两行会让字典的"一行一字段"承诺失效。 */
46
32
  function inlineFieldText(value) {
47
33
  if (typeof value !== 'string')
48
34
  return undefined;
49
35
  const text = value.replace(/\s+/g, ' ').trim();
50
36
  return text.length === 0 ? undefined : text;
51
37
  }
52
- /** 字段字典的递归上界:真实契约最深 3 层,留余量,也不给畸形 schema 无限展开的机会。 */
38
+ /** Recursion bound for field dictionaries; real contracts are at most 3 deep. */
53
39
  const MAX_FIELD_DEPTH = 5;
54
40
  function inspectField(node) {
55
41
  if (!isPlainRecord(node))
@@ -58,7 +44,7 @@ function inspectField(node) {
58
44
  if (Array.isArray(node.oneOf)) {
59
45
  const branches = node.oneOf.filter((branch) => !isPlainRecord(branch) || branch.type !== 'null');
60
46
  const nullable = branches.length !== node.oneOf.length;
61
- // 可空字段的说明写在联合的外侧({ oneOf: [...], description }),内侧分支的说明作兜底。
47
+ // Nullable unions carry their description on the outer node, not the null branch.
62
48
  const note = outerNote === undefined ? undefined : { description: outerNote };
63
49
  if (branches.length === 1)
64
50
  return { ...inspectField(branches[0]), nullable, ...(note ?? {}) };
@@ -79,8 +65,7 @@ function inspectField(node) {
79
65
  if (properties !== undefined) {
80
66
  return { label: typeof node.type === 'string' ? node.type : 'object', nullable: false, nested: properties, ...(description === undefined ? {} : { description }) };
81
67
  }
82
- // 只有 `enum` 没写 `type` 的字段(龙虎榜渠道、板块类型这类):按取值推断类型,并把取值列进说明段。
83
- // 标成 `json` 是话说错——模型会以为取值开放,于是写出目录里不存在的值。
68
+ // Fields with enum but no type: infer type and list values instead of calling them open-ended json.
84
69
  if (typeof node.type !== 'string' && Array.isArray(node.enum) && node.enum.length > 0) {
85
70
  const kinds = new Set(node.enum.map((value) => (typeof value === 'number' && Number.isInteger(value) ? 'integer' : typeof value)));
86
71
  const values = node.enum.join('/');
@@ -101,14 +86,8 @@ function emitFields(properties, prefix, lines, depth) {
101
86
  }
102
87
  }
103
88
  /**
104
- * 把声明好的 `output_schema` 投影成字段字典:一行一个字段 `路径:类型[:说明]`,
105
- * 类型后缀 `?` 表示可为 null,`[].` 表示数组元素的字段,取值受限时说明段给出 `取值 a/b`。
106
- *
107
- * 为什么不直接把 JSON Schema 交给模型(实测):详情体积几乎就等于 `output_schema` 体积,
108
- * 而它按每列 ~55 字符随**列数**线性增长——72 列的可转债档案光 schema 就 4699 字符,第一次
109
- * 注册就打穿单能力 4096 的预算。字典把每列压到 ~15 字符,列数不再卡住补录。
110
- * 权威声明仍住 `output_schema`:护栏与返回值契约照旧按 schema 校验,这里只是给模型看的投影,
111
- * 不是第二份真相。
89
+ * Project output_schema into one line per field. Raw JSON Schema costs ~55 chars per column and
90
+ * blows the per-capability budget; this is display-only, output_schema stays authoritative.
112
91
  */
113
92
  function renderOutputFields(schema) {
114
93
  if (!isPlainRecord(schema))
@@ -142,20 +121,14 @@ function paramsDigest(request) {
142
121
  }
143
122
  return hash.toString(16).padStart(16, '0').repeat(4);
144
123
  }
145
- /** in-flight 合并键:同一 capability+params+task 的并发请求共享同一次执行。 */
124
+ /** In-flight merge key: same capability + params + task share one execution. */
146
125
  function mergeKey(request) {
147
126
  return `${request.capability}:${request.task_id ?? ''}:${stableSerialize(request.params)}`;
148
127
  }
149
128
  function errorMessage(error) {
150
129
  return error instanceof Error ? error.message : String(error);
151
130
  }
152
- /**
153
- * 把上游错误重新包装成 Error 时**保留稳定错误码**。
154
- *
155
- * DatasetStoreError 带 `code`(如 workspace_not_writable / dataset_expired),但此前一律被
156
- * 拍成纯文本 Error,调用方只能从消息前缀里猜。协议要求失败回传携带 `error` 与 `code`,
157
- * 因此这里把 code 原样挂到新 Error 上,工具层与上层消费者都可直接读取。
158
- */
131
+ /** Preserve a stable error `code` when rewrapping upstream errors; callers read it directly. */
159
132
  function errorWithCode(message, source) {
160
133
  const failure = new Error(message);
161
134
  if (source && typeof source === 'object' && typeof source.code === 'string') {
@@ -165,15 +138,11 @@ function errorWithCode(message, source) {
165
138
  return failure;
166
139
  }
167
140
  /**
168
- * Phase 1 数据执行器:FIFO 串行执行外部数据源,成功后由宿主 store 立即
169
- * 原子落盘为不可变 Dataset,只回传 DatasetRef。
170
- *
171
- * 明确不做:长期 raw data 内存缓存、按 data_key 的查询、source_preference 路由。
172
- * 已验证 DatasetRef 的复用由 workspace manifest 提供,内存只保留 in-flight 队列。
141
+ * FIFO executor: success is persisted immediately as an immutable Dataset and only a DatasetRef is
142
+ * returned. In-flight queue is the only in-memory raw data; reuse comes from workspace manifests.
173
143
  */
174
144
  export class DataCollectorHub {
175
145
  queue = [];
176
- /** 正在执行的队列项(执行期间不在 queue 中,但仍参与合并与容量判断)。 */
177
146
  active = null;
178
147
  sources = new Map();
179
148
  cacheLookups = new Map();
@@ -208,13 +177,8 @@ export class DataCollectorHub {
208
177
  this.sources.delete(capability); };
209
178
  }
210
179
  /**
211
- * 入队并阻塞等待完成:先按 capability+params_digest 查找当前 session 下仍有效的
212
- * manifest;force_refresh=true 或没有可复用 Dataset 时才进入数据源队列。相同的
213
- * in-flight 请求仍共享一次执行,成功后立即由宿主 store 原子落盘。
214
- *
215
- * 参数规范化(source 可选)发生在这里:merge key 与 digest 都用规范化后的参数,
216
- * 因此「大小写/空白/重复项不同、语义相同」的请求命中同一个 Dataset。未知
217
- * capability 不做处理,仍由执行阶段抛出统一的 no data source 错误。
180
+ * Enqueue and wait: reuse a valid manifest unless force_refresh; identical in-flight requests share
181
+ * one execution. Params are normalized before merge key and digest.
218
182
  */
219
183
  async request(request, options = {}) {
220
184
  if (!request.capability)
@@ -231,9 +195,7 @@ export class DataCollectorHub {
231
195
  if (normalized.force_refresh !== true && this.store.findLatest) {
232
196
  let lookup = this.cacheLookups.get(key);
233
197
  if (!lookup) {
234
- // 这个 lookup 会被同一 mergeKey 的**所有**并发调用者共享,因此绝不能绑定
235
- // 其中任何一个人的 AbortSignal:首个调用者取消会让其余调用者一起收到同一个
236
- // lookup 失败,而不是各自独立等待。取消只作用于调用者自己的 waiter。
198
+ // Shared by every caller with this mergeKey, so it must not bind any caller's AbortSignal.
237
199
  lookup = this.store.findLatest({
238
200
  session: normalized.session,
239
201
  capability: normalized.capability,
@@ -262,14 +224,12 @@ export class DataCollectorHub {
262
224
  void this.processNext();
263
225
  return pending;
264
226
  }
265
- /** 应用 source 的可选参数规范化;未声明或 capability 未注册时原样返回。 */
266
227
  normalizeRequest(request) {
267
228
  const source = this.sources.get(request.capability);
268
229
  if (!source?.normalizeParams)
269
230
  return request;
270
231
  return { ...request, params: source.normalizeParams(request.params) };
271
232
  }
272
- /** 挂一个等待者并返回其结算承诺;signal 触发时摘除等待者并拒绝(不影响共享执行)。 */
273
233
  awaitSettlement(item, signal) {
274
234
  return new Promise((resolve, reject) => {
275
235
  const waiter = { resolve, reject, signal };
@@ -287,7 +247,6 @@ export class DataCollectorHub {
287
247
  item.waiters.add(waiter);
288
248
  });
289
249
  }
290
- /** 结算一个等待者:无论成败都摘除监听,防止重复结算。 */
291
250
  settleWaiter(item, waiter, outcome) {
292
251
  item.waiters.delete(waiter);
293
252
  if (waiter.onAbort && waiter.signal)
@@ -297,14 +256,7 @@ export class DataCollectorHub {
297
256
  else
298
257
  waiter.reject(outcome.error);
299
258
  }
300
- /**
301
- * 能力目录(模型可见的最外层发现入口):只含 capability、一行摘要与是否分页。
302
- *
303
- * 目录刻意不带 input_schema / output_schema —— 全量 schema 一度达 23KB,
304
- * 会被工具结果剪枝器截掉中间部分,导致排在中间的能力在发现阶段不可见。
305
- * 目录必须一次性全部返回(体积预算由测试守住),详情走 describeCapability。
306
- * 不含任何内部路由字段(name/source/data_key/source_label)。
307
- */
259
+ /** Catalog: capability, one-line summary, paginated only. Full schemas would be pruned; details come from describeCapability. */
308
260
  listCapabilities() {
309
261
  return [...this.sources.values()].map((source) => ({
310
262
  capability: source.schema.capability,
@@ -312,26 +264,16 @@ export class DataCollectorHub {
312
264
  paginated: source.schema.paginated === true,
313
265
  }));
314
266
  }
315
- /** 当前已注册的能力名(用于未知名字的引导与诊断)。 */
316
267
  capabilityNames() {
317
268
  return [...this.sources.keys()];
318
269
  }
319
- /**
320
- * 能力目录的模型可见形态:一行一条 `capability|summary|paginated`(1=分页、0=不分页)。
321
- * 同等信息量下 JSON 数组要 5530 字符(69 条实测,其中 58% 是键名与标点),行编码 2435、
322
- * 均摊从 80 降到 35 字符——目录余量从 +7 条变成 +105 条。信息量没变、依然一次性全量返回,
323
- * §1.1 那条"不分页、不裁剪"的硬约束原样成立。结构化形态仍由 listCapabilities 提供(测试与内部消费)。
324
- */
270
+ /** Model-visible catalog: one `capability|summary|paginated` line each; JSON costs about twice the tokens. */
325
271
  capabilityDirectory() {
326
272
  return this.listCapabilities()
327
273
  .map((entry) => `${encodeDirectoryField(entry.capability)}|${encodeDirectoryField(entry.summary)}|${entry.paginated ? 1 : 0}`)
328
274
  .join('\n');
329
275
  }
330
- /**
331
- * 单个能力的完整契约;名字未注册时返回 undefined,由工具层分类成可引导的错误。
332
- * 返回值只含模型可见字段,是新建的普通对象,不回传内部 live data。
333
- * `output_fields` 是 `output_schema` 的投影:详情体积按列数增长会被宽表打穿,见 renderOutputFields。
334
- */
276
+ /** Single capability contract; undefined for unknown names (the tool layer classifies it). */
335
277
  describeCapability(capability) {
336
278
  const source = this.sources.get(capability);
337
279
  if (!source)
@@ -361,9 +303,7 @@ export class DataCollectorHub {
361
303
  const controller = new AbortController();
362
304
  let timeout;
363
305
  const operation = source.execute(item.request, controller.signal);
364
- // Abort cooperative sources and also release the FIFO worker if a source
365
- // ignores the signal. Observe the underlying promise to avoid an
366
- // unhandled rejection after the timeout race has settled.
306
+ // Abort cooperative sources and release the FIFO worker even if a source ignores the signal.
367
307
  void operation.catch(() => { });
368
308
  const timeoutPromise = new Promise((_, reject) => {
369
309
  timeout = setTimeout(() => {
@@ -413,13 +353,7 @@ export class DataCollectorHub {
413
353
  }
414
354
  }
415
355
  }
416
- /**
417
- * 从数据源声明的行形状推导存储 format 与行数;未声明时退化为形状推断。
418
- *
419
- * 之前只认 `data.item`,于是龙虎榜(行数组在 `stock_items`)被整份判成
420
- * `format: 'json'`,data_junior 连 inspect 都说不可读。行在哪里是数据源的
421
- * 契约知识,不该由存储层猜。
422
- */
356
+ /** Derive storage format and row count from the declared row shape; fall back to inference. */
423
357
  function shapeOf(data, hint) {
424
358
  if (Array.isArray(data))
425
359
  return { format: 'json_rows', rowCount: data.length };
@@ -1,28 +1,14 @@
1
1
  import { type QuerySpec } from './query.js';
2
2
  import type { SessionLike, TimeAxisSnapshot } from './store.js';
3
3
  /**
4
- * 受控查询的**时间轴归一**:唯一实现,`query_dataset` 与 `describe_dataset.queries` 共用。
5
- *
6
- * ⛔ 2026-09-23 事故(会话 `66fa9666`):这一层最初只接在 `query_dataset` 上,`describe_dataset`
7
- * 内嵌的 queries 直接走引擎,于是同一个模型在同一个回合里得到两种行为——
8
- * - `query_dataset`:`">=": "2026-09"` → 正确当作 9 月区间;
9
- * - `describe_dataset.queries`:`">=": "2026"` → 被引擎当**数字 2026** 比较,243 行全中
10
- * (`days_2026: 243`,静默错误);`">=": "2026-07"` → 整条被 `skip_with_warning` 跳过,
11
- * `matched_row_count: 0`(静默空结果)。
12
- * 协议与委派消息都写着"时间筛选直接写日期串",所以**只要有一个入口没接上,模型就一定踩**。
13
- * 新增任何"执行 QuerySpec"的入口都必须复用本模块,不要在别处再写一份。
4
+ * Query-time normalization: the single implementation shared by query_dataset and
5
+ * describe_dataset.queries. Every entry point executing a QuerySpec must call it; an entry that
6
+ * skips it gives date strings a different meaning in the same turn.
14
7
  */
15
8
  /**
16
- * 时间轴归一:把 filter 里的日期写法(`2026-08-23`、`2026-08`、`2026`、`{period:"last_3_months"}`)
17
- * 翻译成该 Dataset 时间列自己的边界值,并给结果补 `*_iso` 可读时间列。
18
- *
19
- * 为什么在工具边界做(真机会话 1e44050e 实测):`date_ms` 列只接受毫秒,模型于是把
20
- * "近 1 月 / 近 3 月 / 52 周 / 年初至今 / 历年 10 月"全部手算成 epoch——单步思考 41,961
21
- * 字符里绝大部分是这类算术,而且算错就是错口径。日历换算是确定性计算,属于宿主:
22
- * 模型只写日历,宿主按**该列观测到的偏移**换算。
23
- *
24
- * 日期串落到数值时间列时**一律响亮失败**:旧路径下 `skip_with_warning` 会把这条 filter
25
- * 整条跳过(`filter X skipped incompatible value`),于是"筛选消失、返回全量"却看起来成功。
9
+ * Translate date-like filter values into this Dataset's time-axis bounds and add `*_iso` result
10
+ * columns. Date strings against a numeric time column fail loudly; a skipped filter would look
11
+ * like a successful full-table result.
26
12
  */
27
13
  export declare function normalizeQueryDates(input: {
28
14
  store: TimeAxisProbe;
@@ -38,7 +24,6 @@ export declare function normalizeQueryDates(input: {
38
24
  time_zone?: string;
39
25
  };
40
26
  }>;
41
- /** 时间轴只读探针:`WorkspaceDatasetStore` 与测试替身都满足这个形状。 */
42
27
  export interface TimeAxisProbe {
43
28
  describeTimeAxis(input: {
44
29
  session: unknown;
@@ -47,11 +32,7 @@ export interface TimeAxisProbe {
47
32
  signal?: AbortSignal;
48
33
  }): Promise<TimeAxisSnapshot | undefined>;
49
34
  }
50
- /**
51
- * 结果补 ISO 时间列:`date_ms: 1787414400000` 旁边给 `date_ms_iso: "2026-08-23"`。
52
- * 只加不删,原值仍是唯一权威(回传口径要求"字段按工具返回原样")。
53
- * 已有同名 `*_iso` 列时不覆盖,避免与 Dataset 自带字段撞名。
54
- */
35
+ /** Add `*_iso` columns without replacing originals; skip an existing column to avoid collisions. */
55
36
  export declare function withIsoTimeColumns(result: Record<string, unknown>, axis: {
56
37
  column: string;
57
38
  time_zone?: string;
@@ -2,28 +2,14 @@ import { MAX_QUERY_FILTERS, DatasetQueryError } from './query.js';
2
2
  import { isRecord } from './args.js';
3
3
  import { isoDateFromEpoch, translateFilterDateValue } from './time-axis.js';
4
4
  /**
5
- * 受控查询的**时间轴归一**:唯一实现,`query_dataset` 与 `describe_dataset.queries` 共用。
6
- *
7
- * ⛔ 2026-09-23 事故(会话 `66fa9666`):这一层最初只接在 `query_dataset` 上,`describe_dataset`
8
- * 内嵌的 queries 直接走引擎,于是同一个模型在同一个回合里得到两种行为——
9
- * - `query_dataset`:`">=": "2026-09"` → 正确当作 9 月区间;
10
- * - `describe_dataset.queries`:`">=": "2026"` → 被引擎当**数字 2026** 比较,243 行全中
11
- * (`days_2026: 243`,静默错误);`">=": "2026-07"` → 整条被 `skip_with_warning` 跳过,
12
- * `matched_row_count: 0`(静默空结果)。
13
- * 协议与委派消息都写着"时间筛选直接写日期串",所以**只要有一个入口没接上,模型就一定踩**。
14
- * 新增任何"执行 QuerySpec"的入口都必须复用本模块,不要在别处再写一份。
5
+ * Query-time normalization: the single implementation shared by query_dataset and
6
+ * describe_dataset.queries. Every entry point executing a QuerySpec must call it; an entry that
7
+ * skips it gives date strings a different meaning in the same turn.
15
8
  */
16
9
  /**
17
- * 时间轴归一:把 filter 里的日期写法(`2026-08-23`、`2026-08`、`2026`、`{period:"last_3_months"}`)
18
- * 翻译成该 Dataset 时间列自己的边界值,并给结果补 `*_iso` 可读时间列。
19
- *
20
- * 为什么在工具边界做(真机会话 1e44050e 实测):`date_ms` 列只接受毫秒,模型于是把
21
- * "近 1 月 / 近 3 月 / 52 周 / 年初至今 / 历年 10 月"全部手算成 epoch——单步思考 41,961
22
- * 字符里绝大部分是这类算术,而且算错就是错口径。日历换算是确定性计算,属于宿主:
23
- * 模型只写日历,宿主按**该列观测到的偏移**换算。
24
- *
25
- * 日期串落到数值时间列时**一律响亮失败**:旧路径下 `skip_with_warning` 会把这条 filter
26
- * 整条跳过(`filter X skipped incompatible value`),于是"筛选消失、返回全量"却看起来成功。
10
+ * Translate date-like filter values into this Dataset's time-axis bounds and add `*_iso` result
11
+ * columns. Date strings against a numeric time column fail loudly; a skipped filter would look
12
+ * like a successful full-table result.
27
13
  */
28
14
  export async function normalizeQueryDates(input) {
29
15
  const filters = input.query.filters;
@@ -39,8 +25,7 @@ export async function normalizeQueryDates(input) {
39
25
  });
40
26
  }
41
27
  catch {
42
- // 探针只服务"日期写法"这一便利:探针不可用(文档型 Dataset、不可读)时保持原样,
43
- // 让真正的查询路径去报它自己的错,不要在这里把错误提前成另一种。
28
+ // Probe failure only affects this convenience translation; let the real query path report its own error.
44
29
  return { query: input.query };
45
30
  }
46
31
  if (snapshot === undefined)
@@ -75,11 +60,7 @@ export async function normalizeQueryDates(input) {
75
60
  axis: { column: snapshot.axis.column, ...(snapshot.axis.time_zone === undefined ? {} : { time_zone: snapshot.axis.time_zone }) },
76
61
  };
77
62
  }
78
- /**
79
- * 结果补 ISO 时间列:`date_ms: 1787414400000` 旁边给 `date_ms_iso: "2026-08-23"`。
80
- * 只加不删,原值仍是唯一权威(回传口径要求"字段按工具返回原样")。
81
- * 已有同名 `*_iso` 列时不覆盖,避免与 Dataset 自带字段撞名。
82
- */
63
+ /** Add `*_iso` columns without replacing originals; skip an existing column to avoid collisions. */
83
64
  export function withIsoTimeColumns(result, axis) {
84
65
  const rows = result.rows;
85
66
  if (!Array.isArray(rows))