dsh-math-calculator 2.1.1 → 2.2.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 (44) hide show
  1. package/CHANGELOG.md +921 -0
  2. package/README.md +64 -50
  3. package/dsh.plugin.json +2 -2
  4. package/lib/__wtest.txt +1 -0
  5. package/lib/awareness.js +69 -20
  6. package/lib/format.js +522 -13
  7. package/lib/formula-library-em.js +428 -0
  8. package/lib/formulas.js +28 -1
  9. package/lib/guards.js +653 -0
  10. package/lib/help.js +509 -0
  11. package/lib/index.js +1357 -212
  12. package/lib/ops/shared.js +547 -6
  13. package/lib/ops/units-op.js +116 -10
  14. package/lib/ops/workspace-op.js +10 -0
  15. package/lib/playbook.js +32 -7
  16. package/lib/types/format.d.ts +31 -1
  17. package/lib/types/formula-library-em.d.ts +28 -0
  18. package/lib/types/formulas.d.ts +2 -0
  19. package/lib/types/guards.d.ts +255 -0
  20. package/lib/types/help.d.ts +70 -0
  21. package/lib/types/index.d.ts +34 -1
  22. package/lib/types/ops/shared.d.ts +87 -3
  23. package/lib/types/playbook.d.ts +28 -0
  24. package/lib/types/units-data.d.ts +253 -0
  25. package/lib/types/units.d.ts +54 -4
  26. package/lib/types/workspace.d.ts +50 -0
  27. package/lib/units-data.js +1037 -0
  28. package/lib/units.js +82 -117
  29. package/lib/workspace.js +54 -0
  30. package/package.json +1 -1
  31. package/src/awareness.ts +69 -20
  32. package/src/format.ts +520 -12
  33. package/src/formula-library-em.ts +435 -0
  34. package/src/formulas.ts +35 -4
  35. package/src/guards.ts +673 -0
  36. package/src/help.ts +543 -0
  37. package/src/index.ts +1492 -213
  38. package/src/ops/shared.ts +589 -9
  39. package/src/ops/units-op.ts +125 -9
  40. package/src/ops/workspace-op.ts +10 -0
  41. package/src/playbook.ts +32 -7
  42. package/src/units-data.ts +1128 -0
  43. package/src/units.ts +149 -130
  44. package/src/workspace.ts +65 -0
@@ -34,7 +34,20 @@ import {
34
34
  import { CalcError, safeNumberForJson, type MathArgs, type Outcome } from './shared.js'
35
35
 
36
36
  /** 量纲的展示顺序(先写日常用的)。 */
37
- const ORDER: Dimension[] = ['length', 'mass', 'time', 'area', 'volume', 'energy', 'power', 'temperature', 'speed']
37
+ const ORDER: Dimension[] = [
38
+ // 力学与几何
39
+ 'length', 'mass', 'time', 'area', 'volume',
40
+ 'energy', 'power', 'temperature', 'speed', 'pressure',
41
+ 'force', 'angle', 'data', 'frequency',
42
+ // 电学
43
+ 'current', 'charge', 'voltage', 'capacitance', 'resistance',
44
+ 'conductance', 'magnetic_flux', 'magnetic_flux_density', 'inductance',
45
+ // 光学
46
+ 'solid_angle', 'luminous_intensity', 'luminous_flux', 'illuminance',
47
+ // 化学与核物理
48
+ 'amount_of_substance', 'activity', 'absorbed_dose',
49
+ 'dose_equivalent', 'catalytic_activity',
50
+ ]
38
51
 
39
52
  /**
40
53
  * 把作者传来的自定义单位规范化。
@@ -144,8 +157,30 @@ export function opUnits(args: MathArgs): Outcome {
144
157
  }
145
158
 
146
159
  // ── 情形 A:列清单 ──
160
+ /**
161
+ * ## 为什么默认只给摘要
162
+ *
163
+ * 单位收到 287 条后,全量展开一次是 17000+ 字符 —— 那是**每轮对话都占上下文**的东西,
164
+ * 而调用方通常只想确认「有没有我要的那条」。
165
+ *
166
+ * 所以默认给「每个量纲几条约数 + 几个样例」,要看全表就传 `unitScale`
167
+ * (如 `unitScale:"length"`)。这与 help 的取向一致:**用到时才读**。
168
+ */
147
169
  if (!hasFrom && !hasTo) {
148
- lines.push('══════ 可用单位 ══════')
170
+ const wantedDimension = typeof args.unitScale === 'string' ? args.unitScale.trim().toLowerCase() : ''
171
+ const expandDimension =
172
+ wantedDimension === ''
173
+ ? null
174
+ : ORDER.find(function (d) { return d === wantedDimension || DIMENSION_LABEL[d] === wantedDimension }) ?? null
175
+ if (wantedDimension !== '' && expandDimension === null) {
176
+ throw new CalcError(
177
+ 'invalid_argument',
178
+ '不认识的量纲「' + wantedDimension + '」',
179
+ '可用: ' + ORDER.map(function (d) { return d + '(' + DIMENSION_LABEL[d] + ')' }).join(' / '),
180
+ )
181
+ }
182
+
183
+ lines.push('══════ 可用单位(共 ' + (UNITS.length + custom.length) + ' 条)══════')
149
184
  lines.push('')
150
185
  if (custom.length > 0) {
151
186
  lines.push('【本项目的自定义单位】')
@@ -161,23 +196,62 @@ export function opUnits(args: MathArgs): Outcome {
161
196
  for (const dimension of ORDER) {
162
197
  const items = [...custom.filter(function (u) { return u.dimension === dimension }), ...UNITS.filter(function (u) { return u.dimension === dimension })]
163
198
  if (items.length === 0) continue
164
- lines.push('【' + DIMENSION_LABEL[dimension] + '】(基准单位 ' + BASE_UNIT[dimension] + ')')
165
- for (const unit of items) {
166
- const aliases = (unit.aliases || []).length > 0 ? '(别名:' + unit.aliases!.join('、') + ')' : ''
199
+ /** 展开只针对点名的那一维;其余给计数与样例。 */
200
+ if (expandDimension === dimension) {
201
+ /**
202
+ * 展开时**按用途分两段**:度量衡在前(绝大多数调用者要这个),
203
+ * 科学单位在后并标明。混排会让「找三丈是多少米」的人翻过一堆 eV。
204
+ */
205
+ const measureItems = items.filter(function (u) { return u.category !== 'science' })
206
+ const scienceItems = items.filter(function (u) { return u.category === 'science' })
167
207
  lines.push(
168
- ' ' + unit.name.padEnd(10) + ' = ' + showFactor(unit.factor).padEnd(22) +
169
- BASE_UNIT[dimension] + aliases,
208
+ '【' + DIMENSION_LABEL[dimension] + '】(基准单位 ' + BASE_UNIT[dimension] + ')' +
209
+ '共 ' + items.length + ' 条:度量衡 ' + measureItems.length + ' / 科学 ' + scienceItems.length,
210
+ )
211
+ for (const unit of measureItems) {
212
+ const aliases = (unit.aliases || []).length > 0 ? '(别名:' + unit.aliases!.join('、') + ')' : ''
213
+ lines.push(
214
+ ' ' + unit.name.padEnd(12) + ' = ' + showFactor(unit.factor).padEnd(22) +
215
+ BASE_UNIT[dimension] + aliases,
216
+ )
217
+ if (unit.note) lines.push(' · ' + unit.note)
218
+ }
219
+ if (scienceItems.length > 0) {
220
+ lines.push(' ── 科学单位(专业计算用,正文里通常不写)──')
221
+ for (const unit of scienceItems) {
222
+ const aliases = (unit.aliases || []).length > 0 ? '(别名:' + unit.aliases!.join('、') + ')' : ''
223
+ lines.push(
224
+ ' ' + unit.name.padEnd(12) + ' = ' + showFactor(unit.factor).padEnd(22) +
225
+ BASE_UNIT[dimension] + aliases,
226
+ )
227
+ }
228
+ }
229
+ lines.push('')
230
+ } else {
231
+ /** 摘要行也标出度量衡/科学的条数——不展开也能看出这条量纲的分界。 */
232
+ const measureCount = items.filter(function (u) { return u.category !== 'science' }).length
233
+ const scienceCount = items.length - measureCount
234
+ const sample = items.slice(0, 5).map(function (u) { return u.name }).join('/')
235
+ lines.push(
236
+ ' ' + DIMENSION_LABEL[dimension].padEnd(4) + BASE_UNIT[dimension].padEnd(6) +
237
+ ('度量衡 ' + measureCount).padEnd(9) +
238
+ ('科学 ' + scienceCount).padEnd(7) + '例:' + sample + (items.length > 5 ? '/…' : ''),
170
239
  )
171
- if (unit.note) lines.push(' · ' + unit.note)
172
240
  }
173
- lines.push('')
174
241
  }
242
+ if (expandDimension === null) {
243
+ lines.push('【看某一类的全部单位】')
244
+ lines.push(' 传 unitScale:"length" / "mass" / "volume" / …(也可用中文名如 "长度")')
245
+ }
246
+ lines.push('')
175
247
  lines.push('【怎么换算】')
176
248
  lines.push(' {operation:"units", unitValue:30, unitFrom:"里", unitTo:"km"}')
177
249
  lines.push(' → 30 里 = 15 km')
178
250
  lines.push('')
179
251
  lines.push('注:中文古制单位历朝不同,内置值取最常见近似;要精确到某朝代,')
180
252
  lines.push(' 用 customUnits 覆盖(如 {name:"里",dimension:"length",factor:576})。')
253
+ lines.push(' 同一名字在两套体系里数值不同的(加仑、担、石、马力),')
254
+ lines.push(' 已分两类收(gal/gal_uk、hp/hp_metric),按 note 区分。')
181
255
  return {
182
256
  result: lines.join('\n'),
183
257
  value: toJsonValue({
@@ -228,6 +302,48 @@ export function opUnits(args: MathArgs): Outcome {
228
302
  }
229
303
  const c = outcome.conversion
230
304
 
305
+ /**
306
+ * `terse`:只回「A = B」一行。
307
+ *
308
+ * ## 为什么让 terse 对 units 也生效
309
+ *
310
+ * 实测反馈(2026-11-06):`{operation:"units", terse:true}` 返回仍是 6 行带框,
311
+ * terse 对 units **没有任何压缩效果** —— 而 units 的输出本来就有
312
+ * 「代入过程 + 注意」两段可省的内容。
313
+ *
314
+ * ## 但**注意**那一段不能省,只压缩格式
315
+ *
316
+ * 与批量一致:terse 承诺的是「省掉标题与统计」,不是省掉风险。
317
+ * 「里在周制约 415 m」这类适用域提示如果被省掉,
318
+ * 调用方会拿一个用错朝代的值去写设定 —— 那比多几行更糟。
319
+ * 所以 terse 下保留内容,只去掉竖排与空行。
320
+ */
321
+ if (args.terse === true) {
322
+ const terseLines: string[] = []
323
+ terseLines.push(show(c.value) + ' ' + c.from.name + ' = ' + show(c.result) + ' ' + c.to.name)
324
+ if (implicitOne) terseLines[0] = '(未给 unitValue,按 1 计)' + terseLines[0]
325
+ /**
326
+ * 适用域提示合并成一行 —— 它们在 terse 下仍必须出现,
327
+ * 只是不再逐条占一行。
328
+ */
329
+ if (c.warnings.length > 0) terseLines.push('⚠ ' + c.warnings.join(';'))
330
+ return {
331
+ result: terseLines.join('\n'),
332
+ value: toJsonValue({
333
+ mode: 'convert',
334
+ dimension: c.dimension,
335
+ value: safeNumberForJson(c.value),
336
+ from: c.from.name,
337
+ to: c.to.name,
338
+ result: safeNumberForJson(c.result),
339
+ steps: c.steps,
340
+ warnings: c.warnings,
341
+ implicitOne: implicitOne,
342
+ terse: true,
343
+ } as never),
344
+ }
345
+ }
346
+
231
347
  lines.push('══════ 单位换算 ══════')
232
348
  lines.push('')
233
349
  if (implicitOne) {
@@ -213,6 +213,16 @@ export function opWorkspace(args: MathArgs): Outcome {
213
213
  next.variables = { ...next.variables, ...args.workspaceVariables }
214
214
  }
215
215
  if (args.description) next.description = args.description
216
+ /**
217
+ * 命名变量是否参与计算(默认关闭)。
218
+ *
219
+ * 单独一个开关而不是「存了就用」,是为了让这条状态变化是**显式的一次动作**:
220
+ * 打开它会让同一个调用在不同工作区状态下给出不同结果,
221
+ * 调用方应当知道自己在换掉什么。
222
+ */
223
+ if (args.useWorkspaceVariables !== undefined) {
224
+ next.useWorkspaceVariables = Boolean(args.useWorkspaceVariables)
225
+ }
216
226
  /**
217
227
  * 显示精度固化。
218
228
  *
package/src/playbook.ts CHANGED
@@ -295,18 +295,43 @@ export function buildPlaybook(state: {
295
295
  * @param state - 工作区状态
296
296
  * @returns 精简提示词
297
297
  */
298
+ /**
299
+ * brief 档的指令集(每轮都随注入下发,**每个字都在付费**)。
300
+ *
301
+ * ## 字数按「使用频率 × 重要程度」分配
302
+ *
303
+ * 曾经的问题是比例失调:整段 817 字符里,「世界推演」占了 360(44%),
304
+ * 而它只对做架空世界设定那类会话有用 —— 算术、单位换算、统计这些
305
+ * **高频场景完全用不到**,却要为它每轮付 360 字符。
306
+ *
307
+ * 现在按三档分配:
308
+ *
309
+ * | 内容 | 频率 | 重要度 | 字数 |
310
+ * |---|---|---|---|
311
+ * | 行为约束(必须调用 / 不要心算) | 每轮 | 极高 | ~40 |
312
+ * | 该用哪个 operation(最常问) | 极高 | 高 | ~70 |
313
+ * | 可信度标签怎么读 | 中 | 极高 | ~60 |
314
+ * | 精度两层 | 中 | 高 | ~50 |
315
+ * | 世界推演分工(低频,压到极限) | 低 | 中 | ~80 |
316
+ *
317
+ * ## 顺手修掉两处自相矛盾
318
+ *
319
+ * 原第 5 条说「输出默认全精度(几十位小数)」、第 6 条说「默认 8 位小数」——
320
+ * 两条并列下发,模型读到的是互相冲突的指令。**8 位才是现状**(2.1.0 起),
321
+ * 「全精度」那句是过时表述,已删。
322
+ *
323
+ * @param state - 工作区状态(改过的常数、自创量)
324
+ * @returns 指令集文本
325
+ */
298
326
  export function buildBriefPlaybook(state: {
299
327
  changedConstants?: string[]
300
328
  customConstants?: number
301
329
  }): string {
302
330
  const out: string[] = []
303
- out.push('**世界推演:底座算数,你判断与表达。**')
304
- out.push('- 该算的调 `derivation`(pipeline/civilization/dead/observer/backSolve/chaos)与 `custom`')
305
- out.push('- 该写的(结语、描写、叙事判断)由你生成,别照抄工具输出')
306
- out.push('- 硬约束(物质不存在等)如实说;叙事选择(要巨兽、要灵能)不反驳,只讲代价')
307
- out.push('- 算不出来的别编,标待定或问用户')
308
- out.push('- 工具输出默认是**全精度**(可能几十位小数),是供你取舍的原料。**保留几位你来定**:读者只要感觉就少给,要核对就多给,要接着算就管够。别把 60 位抄给读者,也别把该精确的整数(如 10^20+5)截成科学计数法')
309
- out.push('- 工具输出默认按**小数点后 8 位**显示(整数永远完整,如 10^20+5 不会被截)。需要更多位就单次传 `displayPrecision`;想全局换一档用 `{operation:"workspace", action2:"set", displayPrecision:N}` 固化一次。注意 `precision`(计算精度)不要调低,它一低大数就会算错')
331
+ // 低频内容压到 120 字符内。四句话的语义一个不少,只是去掉重复的修饰。
332
+ out.push('**世界推演:底座算数,你判断表达。**')
333
+ out.push('- 算:`derivation`(pipeline/backSolve/dead/observer/chaos);写:结语叙事你生成')
334
+ out.push('- 硬约束如实说;叙事选择不反驳只讲代价;算不出的别编,标待定')
310
335
  if ((state.changedConstants ?? []).length > 0) {
311
336
  out.push('- 本区改过常数,先跑 dimensionless 确认不是单位重定义')
312
337
  }