dsh-math-calculator 2.1.0 → 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.
- package/CHANGELOG.md +951 -0
- package/README.md +64 -50
- package/dsh.plugin.json +2 -2
- package/lib/__wtest.txt +1 -0
- package/lib/awareness.js +69 -20
- package/lib/format.js +522 -13
- package/lib/formula-library-em.js +428 -0
- package/lib/formulas.js +28 -1
- package/lib/guards.js +653 -0
- package/lib/help.js +509 -0
- package/lib/index.js +1367 -212
- package/lib/ops/shared.js +547 -6
- package/lib/ops/units-op.js +116 -10
- package/lib/ops/workspace-op.js +10 -0
- package/lib/playbook.js +32 -7
- package/lib/types/format.d.ts +31 -1
- package/lib/types/formula-library-em.d.ts +28 -0
- package/lib/types/formulas.d.ts +2 -0
- package/lib/types/guards.d.ts +255 -0
- package/lib/types/help.d.ts +70 -0
- package/lib/types/index.d.ts +34 -1
- package/lib/types/ops/shared.d.ts +87 -3
- package/lib/types/playbook.d.ts +28 -0
- package/lib/types/units-data.d.ts +253 -0
- package/lib/types/units.d.ts +54 -4
- package/lib/types/workspace.d.ts +50 -0
- package/lib/units-data.js +1037 -0
- package/lib/units.js +82 -117
- package/lib/workspace.js +54 -0
- package/package.json +1 -1
- package/src/awareness.ts +69 -20
- package/src/format.ts +520 -12
- package/src/formula-library-em.ts +435 -0
- package/src/formulas.ts +35 -4
- package/src/guards.ts +673 -0
- package/src/help.ts +543 -0
- package/src/index.ts +1502 -213
- package/src/ops/shared.ts +589 -9
- package/src/ops/units-op.ts +125 -9
- package/src/ops/workspace-op.ts +10 -0
- package/src/playbook.ts +32 -7
- package/src/units-data.ts +1128 -0
- package/src/units.ts +149 -130
- package/src/workspace.ts +65 -0
package/lib/ops/units-op.js
CHANGED
|
@@ -25,7 +25,20 @@ import { toJsonValue } from '../format.js';
|
|
|
25
25
|
import { BASE_UNIT, DIMENSION_LABEL, UNITS, convert, } from '../units.js';
|
|
26
26
|
import { CalcError, safeNumberForJson } from './shared.js';
|
|
27
27
|
/** 量纲的展示顺序(先写日常用的)。 */
|
|
28
|
-
const ORDER = [
|
|
28
|
+
const ORDER = [
|
|
29
|
+
// 力学与几何
|
|
30
|
+
'length', 'mass', 'time', 'area', 'volume',
|
|
31
|
+
'energy', 'power', 'temperature', 'speed', 'pressure',
|
|
32
|
+
'force', 'angle', 'data', 'frequency',
|
|
33
|
+
// 电学
|
|
34
|
+
'current', 'charge', 'voltage', 'capacitance', 'resistance',
|
|
35
|
+
'conductance', 'magnetic_flux', 'magnetic_flux_density', 'inductance',
|
|
36
|
+
// 光学
|
|
37
|
+
'solid_angle', 'luminous_intensity', 'luminous_flux', 'illuminance',
|
|
38
|
+
// 化学与核物理
|
|
39
|
+
'amount_of_substance', 'activity', 'absorbed_dose',
|
|
40
|
+
'dose_equivalent', 'catalytic_activity',
|
|
41
|
+
];
|
|
29
42
|
/**
|
|
30
43
|
* 把作者传来的自定义单位规范化。
|
|
31
44
|
*
|
|
@@ -123,8 +136,24 @@ export function opUnits(args) {
|
|
|
123
136
|
'想看单位清单请不带 unitValue 调用。');
|
|
124
137
|
}
|
|
125
138
|
// ── 情形 A:列清单 ──
|
|
139
|
+
/**
|
|
140
|
+
* ## 为什么默认只给摘要
|
|
141
|
+
*
|
|
142
|
+
* 单位收到 287 条后,全量展开一次是 17000+ 字符 —— 那是**每轮对话都占上下文**的东西,
|
|
143
|
+
* 而调用方通常只想确认「有没有我要的那条」。
|
|
144
|
+
*
|
|
145
|
+
* 所以默认给「每个量纲几条约数 + 几个样例」,要看全表就传 `unitScale`
|
|
146
|
+
* (如 `unitScale:"length"`)。这与 help 的取向一致:**用到时才读**。
|
|
147
|
+
*/
|
|
126
148
|
if (!hasFrom && !hasTo) {
|
|
127
|
-
|
|
149
|
+
const wantedDimension = typeof args.unitScale === 'string' ? args.unitScale.trim().toLowerCase() : '';
|
|
150
|
+
const expandDimension = wantedDimension === ''
|
|
151
|
+
? null
|
|
152
|
+
: ORDER.find(function (d) { return d === wantedDimension || DIMENSION_LABEL[d] === wantedDimension; }) ?? null;
|
|
153
|
+
if (wantedDimension !== '' && expandDimension === null) {
|
|
154
|
+
throw new CalcError('invalid_argument', '不认识的量纲「' + wantedDimension + '」', '可用: ' + ORDER.map(function (d) { return d + '(' + DIMENSION_LABEL[d] + ')'; }).join(' / '));
|
|
155
|
+
}
|
|
156
|
+
lines.push('══════ 可用单位(共 ' + (UNITS.length + custom.length) + ' 条)══════');
|
|
128
157
|
lines.push('');
|
|
129
158
|
if (custom.length > 0) {
|
|
130
159
|
lines.push('【本项目的自定义单位】');
|
|
@@ -139,22 +168,56 @@ export function opUnits(args) {
|
|
|
139
168
|
const items = [...custom.filter(function (u) { return u.dimension === dimension; }), ...UNITS.filter(function (u) { return u.dimension === dimension; })];
|
|
140
169
|
if (items.length === 0)
|
|
141
170
|
continue;
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
171
|
+
/** 展开只针对点名的那一维;其余给计数与样例。 */
|
|
172
|
+
if (expandDimension === dimension) {
|
|
173
|
+
/**
|
|
174
|
+
* 展开时**按用途分两段**:度量衡在前(绝大多数调用者要这个),
|
|
175
|
+
* 科学单位在后并标明。混排会让「找三丈是多少米」的人翻过一堆 eV。
|
|
176
|
+
*/
|
|
177
|
+
const measureItems = items.filter(function (u) { return u.category !== 'science'; });
|
|
178
|
+
const scienceItems = items.filter(function (u) { return u.category === 'science'; });
|
|
179
|
+
lines.push('【' + DIMENSION_LABEL[dimension] + '】(基准单位 ' + BASE_UNIT[dimension] + ')' +
|
|
180
|
+
'共 ' + items.length + ' 条:度量衡 ' + measureItems.length + ' / 科学 ' + scienceItems.length);
|
|
181
|
+
for (const unit of measureItems) {
|
|
182
|
+
const aliases = (unit.aliases || []).length > 0 ? '(别名:' + unit.aliases.join('、') + ')' : '';
|
|
183
|
+
lines.push(' ' + unit.name.padEnd(12) + ' = ' + showFactor(unit.factor).padEnd(22) +
|
|
184
|
+
BASE_UNIT[dimension] + aliases);
|
|
185
|
+
if (unit.note)
|
|
186
|
+
lines.push(' · ' + unit.note);
|
|
187
|
+
}
|
|
188
|
+
if (scienceItems.length > 0) {
|
|
189
|
+
lines.push(' ── 科学单位(专业计算用,正文里通常不写)──');
|
|
190
|
+
for (const unit of scienceItems) {
|
|
191
|
+
const aliases = (unit.aliases || []).length > 0 ? '(别名:' + unit.aliases.join('、') + ')' : '';
|
|
192
|
+
lines.push(' ' + unit.name.padEnd(12) + ' = ' + showFactor(unit.factor).padEnd(22) +
|
|
193
|
+
BASE_UNIT[dimension] + aliases);
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
lines.push('');
|
|
149
197
|
}
|
|
150
|
-
|
|
198
|
+
else {
|
|
199
|
+
/** 摘要行也标出度量衡/科学的条数——不展开也能看出这条量纲的分界。 */
|
|
200
|
+
const measureCount = items.filter(function (u) { return u.category !== 'science'; }).length;
|
|
201
|
+
const scienceCount = items.length - measureCount;
|
|
202
|
+
const sample = items.slice(0, 5).map(function (u) { return u.name; }).join('/');
|
|
203
|
+
lines.push(' ' + DIMENSION_LABEL[dimension].padEnd(4) + BASE_UNIT[dimension].padEnd(6) +
|
|
204
|
+
('度量衡 ' + measureCount).padEnd(9) +
|
|
205
|
+
('科学 ' + scienceCount).padEnd(7) + '例:' + sample + (items.length > 5 ? '/…' : ''));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (expandDimension === null) {
|
|
209
|
+
lines.push('【看某一类的全部单位】');
|
|
210
|
+
lines.push(' 传 unitScale:"length" / "mass" / "volume" / …(也可用中文名如 "长度")');
|
|
151
211
|
}
|
|
212
|
+
lines.push('');
|
|
152
213
|
lines.push('【怎么换算】');
|
|
153
214
|
lines.push(' {operation:"units", unitValue:30, unitFrom:"里", unitTo:"km"}');
|
|
154
215
|
lines.push(' → 30 里 = 15 km');
|
|
155
216
|
lines.push('');
|
|
156
217
|
lines.push('注:中文古制单位历朝不同,内置值取最常见近似;要精确到某朝代,');
|
|
157
218
|
lines.push(' 用 customUnits 覆盖(如 {name:"里",dimension:"length",factor:576})。');
|
|
219
|
+
lines.push(' 同一名字在两套体系里数值不同的(加仑、担、石、马力),');
|
|
220
|
+
lines.push(' 已分两类收(gal/gal_uk、hp/hp_metric),按 note 区分。');
|
|
158
221
|
return {
|
|
159
222
|
result: lines.join('\n'),
|
|
160
223
|
value: toJsonValue({
|
|
@@ -193,6 +256,49 @@ export function opUnits(args) {
|
|
|
193
256
|
throw new CalcError('invalid_argument', outcome.error, outcome.hint);
|
|
194
257
|
}
|
|
195
258
|
const c = outcome.conversion;
|
|
259
|
+
/**
|
|
260
|
+
* `terse`:只回「A = B」一行。
|
|
261
|
+
*
|
|
262
|
+
* ## 为什么让 terse 对 units 也生效
|
|
263
|
+
*
|
|
264
|
+
* 实测反馈(2026-11-06):`{operation:"units", terse:true}` 返回仍是 6 行带框,
|
|
265
|
+
* terse 对 units **没有任何压缩效果** —— 而 units 的输出本来就有
|
|
266
|
+
* 「代入过程 + 注意」两段可省的内容。
|
|
267
|
+
*
|
|
268
|
+
* ## 但**注意**那一段不能省,只压缩格式
|
|
269
|
+
*
|
|
270
|
+
* 与批量一致:terse 承诺的是「省掉标题与统计」,不是省掉风险。
|
|
271
|
+
* 「里在周制约 415 m」这类适用域提示如果被省掉,
|
|
272
|
+
* 调用方会拿一个用错朝代的值去写设定 —— 那比多几行更糟。
|
|
273
|
+
* 所以 terse 下保留内容,只去掉竖排与空行。
|
|
274
|
+
*/
|
|
275
|
+
if (args.terse === true) {
|
|
276
|
+
const terseLines = [];
|
|
277
|
+
terseLines.push(show(c.value) + ' ' + c.from.name + ' = ' + show(c.result) + ' ' + c.to.name);
|
|
278
|
+
if (implicitOne)
|
|
279
|
+
terseLines[0] = '(未给 unitValue,按 1 计)' + terseLines[0];
|
|
280
|
+
/**
|
|
281
|
+
* 适用域提示合并成一行 —— 它们在 terse 下仍必须出现,
|
|
282
|
+
* 只是不再逐条占一行。
|
|
283
|
+
*/
|
|
284
|
+
if (c.warnings.length > 0)
|
|
285
|
+
terseLines.push('⚠ ' + c.warnings.join(';'));
|
|
286
|
+
return {
|
|
287
|
+
result: terseLines.join('\n'),
|
|
288
|
+
value: toJsonValue({
|
|
289
|
+
mode: 'convert',
|
|
290
|
+
dimension: c.dimension,
|
|
291
|
+
value: safeNumberForJson(c.value),
|
|
292
|
+
from: c.from.name,
|
|
293
|
+
to: c.to.name,
|
|
294
|
+
result: safeNumberForJson(c.result),
|
|
295
|
+
steps: c.steps,
|
|
296
|
+
warnings: c.warnings,
|
|
297
|
+
implicitOne: implicitOne,
|
|
298
|
+
terse: true,
|
|
299
|
+
}),
|
|
300
|
+
};
|
|
301
|
+
}
|
|
196
302
|
lines.push('══════ 单位换算 ══════');
|
|
197
303
|
lines.push('');
|
|
198
304
|
if (implicitOne) {
|
package/lib/ops/workspace-op.js
CHANGED
|
@@ -174,6 +174,16 @@ export function opWorkspace(args) {
|
|
|
174
174
|
}
|
|
175
175
|
if (args.description)
|
|
176
176
|
next.description = args.description;
|
|
177
|
+
/**
|
|
178
|
+
* 命名变量是否参与计算(默认关闭)。
|
|
179
|
+
*
|
|
180
|
+
* 单独一个开关而不是「存了就用」,是为了让这条状态变化是**显式的一次动作**:
|
|
181
|
+
* 打开它会让同一个调用在不同工作区状态下给出不同结果,
|
|
182
|
+
* 调用方应当知道自己在换掉什么。
|
|
183
|
+
*/
|
|
184
|
+
if (args.useWorkspaceVariables !== undefined) {
|
|
185
|
+
next.useWorkspaceVariables = Boolean(args.useWorkspaceVariables);
|
|
186
|
+
}
|
|
177
187
|
/**
|
|
178
188
|
* 显示精度固化。
|
|
179
189
|
*
|
package/lib/playbook.js
CHANGED
|
@@ -258,15 +258,40 @@ export function buildPlaybook(state) {
|
|
|
258
258
|
* @param state - 工作区状态
|
|
259
259
|
* @returns 精简提示词
|
|
260
260
|
*/
|
|
261
|
+
/**
|
|
262
|
+
* brief 档的指令集(每轮都随注入下发,**每个字都在付费**)。
|
|
263
|
+
*
|
|
264
|
+
* ## 字数按「使用频率 × 重要程度」分配
|
|
265
|
+
*
|
|
266
|
+
* 曾经的问题是比例失调:整段 817 字符里,「世界推演」占了 360(44%),
|
|
267
|
+
* 而它只对做架空世界设定那类会话有用 —— 算术、单位换算、统计这些
|
|
268
|
+
* **高频场景完全用不到**,却要为它每轮付 360 字符。
|
|
269
|
+
*
|
|
270
|
+
* 现在按三档分配:
|
|
271
|
+
*
|
|
272
|
+
* | 内容 | 频率 | 重要度 | 字数 |
|
|
273
|
+
* |---|---|---|---|
|
|
274
|
+
* | 行为约束(必须调用 / 不要心算) | 每轮 | 极高 | ~40 |
|
|
275
|
+
* | 该用哪个 operation(最常问) | 极高 | 高 | ~70 |
|
|
276
|
+
* | 可信度标签怎么读 | 中 | 极高 | ~60 |
|
|
277
|
+
* | 精度两层 | 中 | 高 | ~50 |
|
|
278
|
+
* | 世界推演分工(低频,压到极限) | 低 | 中 | ~80 |
|
|
279
|
+
*
|
|
280
|
+
* ## 顺手修掉两处自相矛盾
|
|
281
|
+
*
|
|
282
|
+
* 原第 5 条说「输出默认全精度(几十位小数)」、第 6 条说「默认 8 位小数」——
|
|
283
|
+
* 两条并列下发,模型读到的是互相冲突的指令。**8 位才是现状**(2.1.0 起),
|
|
284
|
+
* 「全精度」那句是过时表述,已删。
|
|
285
|
+
*
|
|
286
|
+
* @param state - 工作区状态(改过的常数、自创量)
|
|
287
|
+
* @returns 指令集文本
|
|
288
|
+
*/
|
|
261
289
|
export function buildBriefPlaybook(state) {
|
|
262
290
|
const out = [];
|
|
263
|
-
|
|
264
|
-
out.push('
|
|
265
|
-
out.push('-
|
|
266
|
-
out.push('-
|
|
267
|
-
out.push('- 算不出来的别编,标待定或问用户');
|
|
268
|
-
out.push('- 工具输出默认是**全精度**(可能几十位小数),是供你取舍的原料。**保留几位你来定**:读者只要感觉就少给,要核对就多给,要接着算就管够。别把 60 位抄给读者,也别把该精确的整数(如 10^20+5)截成科学计数法');
|
|
269
|
-
out.push('- 工具输出默认按**小数点后 8 位**显示(整数永远完整,如 10^20+5 不会被截)。需要更多位就单次传 `displayPrecision`;想全局换一档用 `{operation:"workspace", action2:"set", displayPrecision:N}` 固化一次。注意 `precision`(计算精度)不要调低,它一低大数就会算错');
|
|
291
|
+
// 低频内容压到 120 字符内。四句话的语义一个不少,只是去掉重复的修饰。
|
|
292
|
+
out.push('**世界推演:底座算数,你判断表达。**');
|
|
293
|
+
out.push('- 算:`derivation`(pipeline/backSolve/dead/observer/chaos);写:结语叙事你生成');
|
|
294
|
+
out.push('- 硬约束如实说;叙事选择不反驳只讲代价;算不出的别编,标待定');
|
|
270
295
|
if ((state.changedConstants ?? []).length > 0) {
|
|
271
296
|
out.push('- 本区改过常数,先跑 dimensionless 确认不是单位重定义');
|
|
272
297
|
}
|
package/lib/types/format.d.ts
CHANGED
|
@@ -51,9 +51,10 @@ export declare function isIntegerLike(value: any): boolean;
|
|
|
51
51
|
* @param value - 待格式化的值
|
|
52
52
|
* @param precision - 计算精度(BigNumber 有效位数)
|
|
53
53
|
* @param displayPrecision - **小数点后保留几位**;省略则输出完整精度
|
|
54
|
+
* @param respectExplicitUnit - 调用方是否在表达式里**显式写了目标单位**(`.to('cm')`)
|
|
54
55
|
* @returns 显示串
|
|
55
56
|
*/
|
|
56
|
-
export declare function formatValue(math: any, value: any, precision: number, displayPrecision?: number): string;
|
|
57
|
+
export declare function formatValue(math: any, value: any, precision: number, displayPrecision?: number, respectExplicitUnit?: boolean): string;
|
|
57
58
|
/**
|
|
58
59
|
* 把 mathjs 的任意返回值转成可 JSON 化的值。
|
|
59
60
|
* Matrix / Complex / Unit / Fraction / BigNumber / Range / ResultSet 分别处理。
|
|
@@ -61,6 +62,35 @@ export declare function formatValue(math: any, value: any, precision: number, di
|
|
|
61
62
|
export declare function toJsonValue(value: any, depth?: number, seen?: Set<unknown>): JsonValue;
|
|
62
63
|
/** 取 ResultSet 的最后一个值(`a=1; b=2; a+b` → 3)。 */
|
|
63
64
|
export declare function unwrapResult(value: any): any;
|
|
65
|
+
/**
|
|
66
|
+
* 判断一个 mathjs 原始返回值里是否含非有限数(Infinity / -Infinity / NaN)。
|
|
67
|
+
*
|
|
68
|
+
* ## 为什么不能用序列化后的文本判断
|
|
69
|
+
*
|
|
70
|
+
* 既有实现 `containsNonFinite` 只认**恰好等于** `"Infinity"` 的字符串。
|
|
71
|
+
* 而带单位的结果序列化成 `"Infinity g"` —— 于是:
|
|
72
|
+
*
|
|
73
|
+
* ```
|
|
74
|
+
* (1/0)*2 kg → ok=true,result "Infinity g" ← 漏检
|
|
75
|
+
* 2 kg → ok=true,result "2 kg" ← 正常
|
|
76
|
+
* ```
|
|
77
|
+
*
|
|
78
|
+
* 同一件事在裸数字上拦得住、在带单位值上拦不住,是**判据挑错了层**:
|
|
79
|
+
* 文本层只能看见「长得像」,值层才知道「是不是」。
|
|
80
|
+
*
|
|
81
|
+
* ## Unit 的非有限数藏在 `value` 里
|
|
82
|
+
*
|
|
83
|
+
* ```
|
|
84
|
+
* (1/0)*2 kg → Unit { value: BigNumber(Infinity), units: [g] }
|
|
85
|
+
* (2/0) kg to g → Unit { value: BigNumber(Infinity) }
|
|
86
|
+
* ```
|
|
87
|
+
*
|
|
88
|
+
* 而正则读文本时那个 `g` 会把判定带偏。所以逐层下钻到真正的数字。
|
|
89
|
+
*
|
|
90
|
+
* @param value - mathjs 的原始返回值(不是 toJsonValue 之后的)
|
|
91
|
+
* @returns 含非有限数时为 true
|
|
92
|
+
*/
|
|
93
|
+
export declare function isNonFiniteValue(value: any, depth?: number): boolean;
|
|
64
94
|
/** 递归判断一个 JSON 值里是否含非有限数字。 */
|
|
65
95
|
export declare function containsNonFinite(value: JsonValue | undefined): boolean;
|
|
66
96
|
/**
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 公式库:电学 / 光学 / 核物理与化学计量。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么单独成批
|
|
5
|
+
*
|
|
6
|
+
* 单位表补齐了 18 个量纲(电流、电压、电阻、磁通密度、照度、活度、
|
|
7
|
+
* 吸收剂量、催化活度……),但**公式库几乎空白**:
|
|
8
|
+
*
|
|
9
|
+
* | 量纲 | 补齐前 |
|
|
10
|
+
* |---|---|
|
|
11
|
+
* | 磁通密度 / 立体角 / 发光强度 / 照度 | 0 条 |
|
|
12
|
+
* | 放射性活度 / 吸收剂量 / 剂量当量 / 催化活度 | 0 条 |
|
|
13
|
+
*
|
|
14
|
+
* 有单位没公式 = 那批单位永远产出不出来。这一批按**该量纲最常被问到的量**
|
|
15
|
+
* 补齐,每条都能独立算出该量纲的一个值。
|
|
16
|
+
*
|
|
17
|
+
* ## 收录判据(与其它批次一致)
|
|
18
|
+
*
|
|
19
|
+
* 能写成闭合式、参数现实可得、不依赖外部数据集。
|
|
20
|
+
* 经验式必须标注适用域;只有在地球成立的,desc 里写明。
|
|
21
|
+
*
|
|
22
|
+
* @module dsh-math-calculator/formula-library-em
|
|
23
|
+
*/
|
|
24
|
+
import { type FormulaDef } from './formula-library.js';
|
|
25
|
+
/**
|
|
26
|
+
* 电学 / 光学 / 核物理 / 化学计量公式。
|
|
27
|
+
*/
|
|
28
|
+
export declare const EM_OPTICAL_NUCLEAR_FORMULAS: FormulaDef[];
|
package/lib/types/formulas.d.ts
CHANGED
|
@@ -47,6 +47,8 @@ export declare function resolveParams(formula: FormulaDef, overrides: Overrides,
|
|
|
47
47
|
values: Record<string, unknown>;
|
|
48
48
|
overridden: string[];
|
|
49
49
|
missing: string[];
|
|
50
|
+
/** 传了但公式里没有的参数名 —— 会被忽略,必须上报。 */
|
|
51
|
+
unknownKeys: string[];
|
|
50
52
|
};
|
|
51
53
|
/**
|
|
52
54
|
* 把参数表拼成 mathjs 可用的 scope。
|
|
@@ -0,0 +1,255 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 防呆层:在数字旁边多放一句「这个数看起来不对劲」。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么需要这一层
|
|
5
|
+
*
|
|
6
|
+
* 一份实测复盘文档记录了 200 余条表达式的使用,其中**真正算错的只有单位口径类**,
|
|
7
|
+
* 但代价最贵的不是算错,而是**算对了却没人觉得不对**:
|
|
8
|
+
*
|
|
9
|
+
* ```
|
|
10
|
+
* 2645.6*28*3600*8000/1e9 → 2133.41184
|
|
11
|
+
* 调用者贴上「万吨」的标签 → 以此为据指控原文「错了 10 倍」
|
|
12
|
+
* 真相:2133 是「千吨」,万吨应为 /1e10 → 213.34
|
|
13
|
+
* ```
|
|
14
|
+
*
|
|
15
|
+
* 返回体当时只有一行裸数字,**既不说量级、也不说 label 与结果是否对得上**。
|
|
16
|
+
*
|
|
17
|
+
* ## 这一层做什么、不做什么
|
|
18
|
+
*
|
|
19
|
+
* **做**:数量级标注、label 与结果的量级比对、同批次一致性比对。
|
|
20
|
+
* **不做**:替调用者判断结论对错 —— 那需要动机与上下文,工具拿不到。
|
|
21
|
+
*
|
|
22
|
+
* 判据必须**保守**:只在「label 明确声称了一个量级词」且「结果差得离谱」时提示。
|
|
23
|
+
* 宁可漏报,不可误报 —— 误报会让整条通道被关掉,比漏报更糟。
|
|
24
|
+
*
|
|
25
|
+
* @module dsh-math-calculator/guards
|
|
26
|
+
*/
|
|
27
|
+
/**
|
|
28
|
+
* 从 label 里认出量级词。
|
|
29
|
+
*
|
|
30
|
+
* @param label - 条目的 label(如「年耗N2万吨」「年发电量亿度」)
|
|
31
|
+
* @returns 命中的量级词与幂次;没命中返回 null
|
|
32
|
+
*/
|
|
33
|
+
export declare function magnitudeOfLabel(label: string): {
|
|
34
|
+
word: string;
|
|
35
|
+
power: number;
|
|
36
|
+
} | null;
|
|
37
|
+
/**
|
|
38
|
+
* 给数值加一个量级标注,如 `2.13e5 (~10⁵)`。
|
|
39
|
+
*
|
|
40
|
+
* ## 为什么不直接格式化成分数/万/亿
|
|
41
|
+
*
|
|
42
|
+
* 因为工具不知道量纲,也就不知道该用「万吨」还是「万度」还是「万焦」。
|
|
43
|
+
* 只给**纯数量级**(10 的幂),调用方自己配上单位 —— 这不容易出错。
|
|
44
|
+
*
|
|
45
|
+
* @param value - 已格式化的结果文本(如「2133.41184」)
|
|
46
|
+
* @returns 带量级标注的文本;值不是有限数或量级为 1 时原样返回
|
|
47
|
+
*/
|
|
48
|
+
export declare function annotateMagnitude(value: string): string;
|
|
49
|
+
/**
|
|
50
|
+
* 三角函数用了「无单位角度」时,补一句「按弧度计算」。
|
|
51
|
+
*
|
|
52
|
+
* ## 为什么值得加(成本极低)
|
|
53
|
+
*
|
|
54
|
+
* 实测反馈(2026-11-06 第二份):
|
|
55
|
+
*
|
|
56
|
+
* ```
|
|
57
|
+
* sin(30) = -0.98803162 ← 弧度
|
|
58
|
+
* sin(30 deg) = 0.5 ← 加单位才对
|
|
59
|
+
* ```
|
|
60
|
+
*
|
|
61
|
+
* 中文工程语境里 `sin(30)` 的默认读法是 **30 度**。
|
|
62
|
+
* 按数学惯例走弧度是对的,但**不给提示**就是静默错误:
|
|
63
|
+
* `-0.988` 看起来像个正常数,用户不会察觉自己问的是 30 度。
|
|
64
|
+
*
|
|
65
|
+
* ## 判据:只在「像角度但没写 deg」时提示
|
|
66
|
+
*
|
|
67
|
+
* · 函数是三角函数(sin/cos/tan/asin…)
|
|
68
|
+
* · 参数是裸数字(无 deg/rad/单位)
|
|
69
|
+
* · 数值 ∈ [0, 360](角度制下的常见范围)
|
|
70
|
+
*
|
|
71
|
+
* 三条同时满足才提示 —— 幂函数、双曲函数、`sin(1e6)` 之类一律不注(那是真的用弧度)。
|
|
72
|
+
*
|
|
73
|
+
* @param expression - 归一化后的表达式
|
|
74
|
+
* @returns 提示文本;不需要时返回空串
|
|
75
|
+
*/
|
|
76
|
+
export declare function checkAngleUnit(expression: string): string;
|
|
77
|
+
/**
|
|
78
|
+
* 给带 SI 前缀的结果补一句中文量级参照。
|
|
79
|
+
*
|
|
80
|
+
* ## 为什么需要它
|
|
81
|
+
*
|
|
82
|
+
* 实测反馈(2026-11-06 第二份):
|
|
83
|
+
*
|
|
84
|
+
* ```
|
|
85
|
+
* 2.66e8 * 330 kg = 87.78 Tg ← 太克,现实中没人这么说
|
|
86
|
+
* (2.66e8*330 kg) in t = 8.778e+7 t
|
|
87
|
+
* ```
|
|
88
|
+
*
|
|
89
|
+
* 「?87.78?Tg」读不出「8,778 万吨」——而中文语料里这个量级就叫「亿吨」。
|
|
90
|
+
* 报告作者两次把「吨」当成「亿吨」,起因正是**输出里没有一个中文量级锚点**。
|
|
91
|
+
*
|
|
92
|
+
* ## 为什么不动 prest? 而是加注释
|
|
93
|
+
*
|
|
94
|
+
* 换前缀策略(k/M/G 封顶)会改变所有既有输出,破坏依赖精确文本的调用方;
|
|
95
|
+
* 而「加一句参照」是**追加信息**,不改变原有数值文本。
|
|
96
|
+
* 按项目一贯取舍:能加注释就不改主文本。
|
|
97
|
+
*
|
|
98
|
+
* ## 只认 SI 前缀 + 简单单位,且只在中英文语料真正会用的量级出现
|
|
99
|
+
*
|
|
100
|
+
/**
|
|
101
|
+
* 带单位的结果:给 AI 补一句「这个数有多大、等价于什么」。
|
|
102
|
+
*
|
|
103
|
+
* ## 为什么需要它
|
|
104
|
+
*
|
|
105
|
+
* 实测:`evaluate` 带单位的结果只有裸文本,`notice` 为空:
|
|
106
|
+
*
|
|
107
|
+
* ```
|
|
108
|
+
* 2.66e8 * 330 kg → 87.78 Mt
|
|
109
|
+
* ```
|
|
110
|
+
*
|
|
111
|
+
* 而 `units` 操作会给出【代入过程】与【注意】。两者信息量差得远:
|
|
112
|
+
* 模型看到 `87.78 Mt` 并不知道它是 8778 万吨、量级是 10⁷ 吨。
|
|
113
|
+
*
|
|
114
|
+
* 用户原话:「面向 AI 的运算信息最好把信息注释写到位。」
|
|
115
|
+
*
|
|
116
|
+
* ## 补什么
|
|
117
|
+
*
|
|
118
|
+
* 三件事,都是模型判断「这个数合不合理」时真正要用的:
|
|
119
|
+
* 1. **展开成基准单位**(`= 8.778e7 t`)—— 便于和别的数比
|
|
120
|
+
* 2. **中文量级说法**(`约 8778 万吨`)—— 中文语料的习惯表达
|
|
121
|
+
* 3. **量级**(`10⁷`)—— 一眼判断大小
|
|
122
|
+
*
|
|
123
|
+
* ## 只对「带单位」的结果补
|
|
124
|
+
*
|
|
125
|
+
* 纯数字走 `(~10^N)` 标注那条路,不重复;复合单位(`m/s^2`)不补 ——
|
|
126
|
+
* 量级词在那个位置不适用。
|
|
127
|
+
*
|
|
128
|
+
* @param text - 已格式化的结果文本
|
|
129
|
+
* @returns 说明文本;不需要时返回空串
|
|
130
|
+
*/
|
|
131
|
+
export declare function explainUnitResult(text: string): string;
|
|
132
|
+
/**
|
|
133
|
+
* 给带 SI 前缀的结果补一句中文量级参照。
|
|
134
|
+
*
|
|
135
|
+
* ## 为什么需要它
|
|
136
|
+
*
|
|
137
|
+
* 实测反馈:`2.66e8 * 330 kg` 曾被输出成 `87.78 Tg`(太克)——
|
|
138
|
+
* 而中文语料里这个量级叫「8778 万吨」。单位归位后 `Tg` 不再出现,
|
|
139
|
+
* 但这条注释仍然作为**兜底**保留:万一某种组合漏出了冷门前缀,它会补一句人话。
|
|
140
|
+
*
|
|
141
|
+
* ## 只认 SI 前缀 + 简单单位
|
|
142
|
+
*
|
|
143
|
+
* · 复合单位(kg/m³、m/s)不注 —— 量级词在那个位置不适用
|
|
144
|
+
*
|
|
145
|
+
* @param text - 已格式化的结果(如 `87.78 Tg`)
|
|
146
|
+
* @returns 带中文参照的文本;不需要时原样返回
|
|
147
|
+
*/
|
|
148
|
+
export declare function annotateMetricPrefix(text: string): string;
|
|
149
|
+
/**
|
|
150
|
+
* 检查 label 声称的量级与结果是否对得上。
|
|
151
|
+
*
|
|
152
|
+
* ## 判据要松
|
|
153
|
+
*
|
|
154
|
+
* 「万吨」标 213.34 —— 213 是**吨**为单位时的数值,而 label 说的是「万吨」,
|
|
155
|
+
* 也就是 213 万吨 = 2.13×10⁶ 吨。这种「label 带量级词但数值本身没到量级」
|
|
156
|
+
* 恰恰**不是**错误:数值 213 + 单位「万吨」= 213 万吨,是正常的。
|
|
157
|
+
*
|
|
158
|
+
* 所以判据不能是「结果是否达到 10⁴」,而是:
|
|
159
|
+
*
|
|
160
|
+
* · 若把结果**按裸数字直接读成该量级**(213 × 10⁴ = 2.13e6),
|
|
161
|
+
* 与「结果本身就是最终量」的读法相比差很多 —— 这时才提示可能混了换算。
|
|
162
|
+
*
|
|
163
|
+
* 实际上更有价值的是**同批一致性**:一批里所有「年耗(万吨)」条目
|
|
164
|
+
* 都落在同一量级、却与另一批同义条目的量级差 10 倍 —— 见 `findBatchAnomalies`。
|
|
165
|
+
*
|
|
166
|
+
* 因此这里只做一件事:**提示 label 里含量级词,提醒调用方核对换算**。
|
|
167
|
+
* 不做数值比对(容易误报)。
|
|
168
|
+
*
|
|
169
|
+
* @param label - 条目 label
|
|
170
|
+
* @param result - 结果文本
|
|
171
|
+
* @returns 提示文本;无需提示时返回空串
|
|
172
|
+
*/
|
|
173
|
+
export declare function checkLabelMagnitude(label: string, result: string): string;
|
|
174
|
+
/**
|
|
175
|
+
* 调用方给的锚点(`sanity` 字段)。
|
|
176
|
+
*
|
|
177
|
+
* ## 这一层的定位
|
|
178
|
+
*
|
|
179
|
+
* 实测报告(2026-11-06)原话:
|
|
180
|
+
*
|
|
181
|
+
* > 工具能算出 `1e8 * 25000 = 2.5e12`,但**不会提醒「这个数比帝国星界军总人口
|
|
182
|
+
* > (1e10)还大 250 倍」**。
|
|
183
|
+
*
|
|
184
|
+
* 工具不知道 1e10 是什么,也不该知道 —— 那需要先检索、再理解语义。
|
|
185
|
+
* 但**调用方知道**:它刚查过那个锚点,或者它本来就在自己的设定文档里。
|
|
186
|
+
*
|
|
187
|
+
* 所以这一层做的是:**把「我已经知道的常识」交给工具,让工具做它擅长的事——比对。**
|
|
188
|
+
* 判据来自人,执行来自工具。不猜。
|
|
189
|
+
*/
|
|
190
|
+
export interface SanitySpec {
|
|
191
|
+
/** 上限:结果超过它时提示。 */
|
|
192
|
+
max?: number;
|
|
193
|
+
/** 下限:结果低于它时提示。 */
|
|
194
|
+
min?: number;
|
|
195
|
+
/** 期望量级(10 的幂)。例如 3 表示「应该在 10³ 量级」。 */
|
|
196
|
+
expect?: number;
|
|
197
|
+
/** 提示时引用的依据(如「帝国星界军总规模」)。不给就说「设定的上限」。 */
|
|
198
|
+
note?: string;
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* 比对锚点,返回提示文本。
|
|
202
|
+
*
|
|
203
|
+
* ## 为什么不处理非有限数
|
|
204
|
+
*
|
|
205
|
+
* `1/0` 得 Infinity。锚点比较对它没有意义(`Infinity > 1e10` 恒真,
|
|
206
|
+
* 但那条提示毫无信息量)。非有限数该由另一条通道报(数值型 operation 用非有限数
|
|
207
|
+
* 本身就该报错),锚点这里静默跳过。
|
|
208
|
+
*
|
|
209
|
+
* ## 为什么只比数值,不管量纲
|
|
210
|
+
*
|
|
211
|
+
* `9e7 kg` 与 `max:1e10` 的语义可能是「9e9 克」——量纲不同,锚点不该管。
|
|
212
|
+
* 这条限制必须写进 help,否则会变成新的误报源。
|
|
213
|
+
*
|
|
214
|
+
* ## expect 的判据是「偏离 ≥1 个数量级」
|
|
215
|
+
*
|
|
216
|
+
* 因为调用方通常只对**量级**有把握(「这是千吨级的」),对具体值没有。
|
|
217
|
+
* 差 0 个数量级(同量级)不报;差 1 个及以上才报。
|
|
218
|
+
*
|
|
219
|
+
* @param spec - 调用方给的锚点
|
|
220
|
+
* @param result - 结果文本(可能是「123」「123 kg」「1.2e5」)
|
|
221
|
+
* @returns 提示文本;无需提示时返回空串
|
|
222
|
+
*/
|
|
223
|
+
export declare function checkAnchors(spec: unknown, result: string): string;
|
|
224
|
+
/**
|
|
225
|
+
* 一条待检查的批量结果。
|
|
226
|
+
*/
|
|
227
|
+
export interface BatchEntryForGuard {
|
|
228
|
+
/** 条目 label(可能为空)。 */
|
|
229
|
+
label?: string;
|
|
230
|
+
/** 结果文本(失败条目不要传)。 */
|
|
231
|
+
result: string;
|
|
232
|
+
}
|
|
233
|
+
/**
|
|
234
|
+
* 同批次量级一致性比对。
|
|
235
|
+
*
|
|
236
|
+
* ## 这是文档里最想要的一条
|
|
237
|
+
*
|
|
238
|
+
* 真实案例:四条「年耗/年产(万吨)」全部用了 `/1e9` 而不是 `/1e10`,
|
|
239
|
+
* 全部偏大 10 倍,全部报「成功」。
|
|
240
|
+
*
|
|
241
|
+
* 单条看不出问题(2133 看起来就是个正常数),但**四条一起就该看出来** ——
|
|
242
|
+
* 它们全落在同一量级,而那个量级与该组共同声称的单位不匹配。
|
|
243
|
+
*
|
|
244
|
+
* ## 判据
|
|
245
|
+
*
|
|
246
|
+
* 1. 按 label 里的量级词分组(同组意味着同一个数量级口径)
|
|
247
|
+
* 2. 组内 ≥2 条,且量级**一致**(说明是同一个系统性偏差)
|
|
248
|
+
* 3. 该量级 ≥ label 声称的量级(说明裸数字本身就跨了那个量级 → 可疑)
|
|
249
|
+
*
|
|
250
|
+
* 三条都满足才提示。这样误报率极低。
|
|
251
|
+
*
|
|
252
|
+
* @param entries - 本次批量里成功的条目
|
|
253
|
+
* @returns 提示文本;无需提示时返回空串
|
|
254
|
+
*/
|
|
255
|
+
export declare function findBatchAnomalies(entries: BatchEntryForGuard[]): string;
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* 分节帮助系统。
|
|
3
|
+
*
|
|
4
|
+
* ## 为什么把摊平的 help 拆成目录
|
|
5
|
+
*
|
|
6
|
+
* 旧的 `opHelp()` 是**一整块 5900 字符**,每次调用都付全量成本。实测分段统计:
|
|
7
|
+
*
|
|
8
|
+
* ```
|
|
9
|
+
* 世界观推演 3039 字符 ← 占 52%,但只在少数调用里用到
|
|
10
|
+
* 求值与代数 612
|
|
11
|
+
* 数据管理 557
|
|
12
|
+
* 公式库 337
|
|
13
|
+
* 其余各节合计 700 左右
|
|
14
|
+
* ```
|
|
15
|
+
*
|
|
16
|
+
* 算一条 `1/3` 也要读 3039 字符的 derivation 子命令表与 world 字段表。
|
|
17
|
+
* 而**平均只查一节时是 426 字符 —— 全量的 7%**。
|
|
18
|
+
*
|
|
19
|
+
* ## 三层结构
|
|
20
|
+
*
|
|
21
|
+
* ```
|
|
22
|
+
* 第1层 help → 目录(节名 + 一句话简介 + 常见别名)
|
|
23
|
+
* 第2层 help, helpTopic:"world" → 该节完整正文
|
|
24
|
+
* 第3层 help, helpTopic:"world" → 正文里含逐字段表(默认值/单位/坑)
|
|
25
|
+
* ```
|
|
26
|
+
*
|
|
27
|
+
* 目录层只保证「AI 知道有什么功能」,所以每节的 `summary` 必须**说清用途**:
|
|
28
|
+
* 简介写得太省,AI 就不会去查,那个功能等于不存在。
|
|
29
|
+
*
|
|
30
|
+
* ## 正文可以写得比旧版更细
|
|
31
|
+
*
|
|
32
|
+
* 旧版为压 6000 字符上限,把字段表、默认值、常见坑都砍了。
|
|
33
|
+
* 现在每节按需取用,**单节详细程度不再受总量约束** —— 只在真用那一节时付费。
|
|
34
|
+
*
|
|
35
|
+
* @module dsh-math-calculator/help
|
|
36
|
+
*/
|
|
37
|
+
import type { Outcome } from './ops/shared.js';
|
|
38
|
+
/**
|
|
39
|
+
* 一节帮助:目录简介 + 正文生成器。
|
|
40
|
+
*
|
|
41
|
+
* `body` 用函数而不是字符串:正文里有 `FORMULAS.length`、
|
|
42
|
+
* `DERIVATION_SUBCOMMANDS.length` 这类**运行期才知道的数**,写成函数才能取到真值。
|
|
43
|
+
*/
|
|
44
|
+
export interface HelpTopic {
|
|
45
|
+
/** 目录键(`helpTopic` 取它)。 */
|
|
46
|
+
key: string;
|
|
47
|
+
/** 中文标题。 */
|
|
48
|
+
title: string;
|
|
49
|
+
/** 一句话简介:这节解决什么问题(目录层唯一可见的信息)。 */
|
|
50
|
+
summary: string;
|
|
51
|
+
/** 常见别名,便于用直觉词命中也(如「概率」命中 dist)。 */
|
|
52
|
+
aliases?: string[];
|
|
53
|
+
/** 该节正文。 */
|
|
54
|
+
body: () => string;
|
|
55
|
+
}
|
|
56
|
+
/**
|
|
57
|
+
* 各节注册表。**顺序即目录顺序**,而顺序决定「AI 会不会翻到它」。
|
|
58
|
+
*
|
|
59
|
+
* 排前面的是高频或高风险节:`evaluate` 最常用、`world` 体量最大。
|
|
60
|
+
* `recipe` 放最后是因为它是跨节组合,读它之前一般已经读了几节。
|
|
61
|
+
*/
|
|
62
|
+
export declare const HELP_TOPICS: HelpTopic[];
|
|
63
|
+
/**
|
|
64
|
+
* 帮助入口。
|
|
65
|
+
*
|
|
66
|
+
* @param topic - 可选。给则返回该节正文;不给返回目录。
|
|
67
|
+
* 认不出的 topic 不报错 —— 回目录并说明没这一节,
|
|
68
|
+
* 顺带列出可选 key。报错会打断调用方的探索,目录更能自救。
|
|
69
|
+
*/
|
|
70
|
+
export declare function opHelp(topic?: string): Outcome;
|