mulmoclaude 2.5.0 → 2.6.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/README.md +1 -1
- package/client/assets/{index-BII5bTJI.css → index-BA0fpWIZ.css} +1 -1
- package/client/assets/{index-CoMmb_Su.js → index-gnbD2OHU.js} +671 -581
- package/client/assets/{marp-uBg3qXiP.js → marp-BVojbGre.js} +1 -1
- package/client/index.html +2 -2
- package/package.json +8 -7
- package/server/agent/activeTools.ts +14 -1
- package/server/api/routes/plugins.ts +12 -0
- package/server/build/mcp-server.mjs +653 -557
- package/server/index.ts +4 -0
- package/server/plugins/kentei-builtin.ts +47 -0
- package/src/config/roles.ts +2 -1
- package/src/main.ts +1 -0
- package/src/plugins/_generated/metas.ts +2 -0
- package/src/plugins/_generated/registrations.ts +2 -0
- package/src/plugins/_generated/server-bindings.ts +3 -0
- package/src/plugins/manageKenteiScript/definition.ts +20 -0
- package/src/plugins/manageKenteiScript/index.ts +39 -0
- package/src/plugins/manageKenteiScript/meta.ts +13 -0
- package/src/plugins/spreadsheet/engine/cellBuilder.ts +10 -2
- package/src/plugins/spreadsheet/engine/evaluator.ts +10 -5
- package/src/plugins/spreadsheet/engine/registry.ts +5 -0
package/server/index.ts
CHANGED
|
@@ -59,6 +59,10 @@ import "./plugins/html-builtin.js";
|
|
|
59
59
|
// presentShapeScript View's useRuntime().dispatch({ kind }) resolves — the
|
|
60
60
|
// source editor's load / save against artifacts/shapes/.
|
|
61
61
|
import "./plugins/shapescript-builtin.js";
|
|
62
|
+
// Side-effect: registers the built-in "kentei" dispatch handler so the
|
|
63
|
+
// manageKenteiScript View's useRuntime().dispatch({ kind: "grade" }) resolves
|
|
64
|
+
// (JEV grading with the server's TYPESAFE_API_KEY).
|
|
65
|
+
import "./plugins/kentei-builtin.js";
|
|
62
66
|
import { enableShapeScriptManifold } from "./plugins/shapescript-csg.js";
|
|
63
67
|
import { loadRuntimePlugins } from "./plugins/runtime-loader.js";
|
|
64
68
|
import { evaluateDevPluginGate, loadDevPlugins, parseDevPluginsEnv } from "./plugins/dev-loader.js";
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
// MulmoClaude's host wiring for the manageKenteiScript plugin (`kenteiscript`
|
|
2
|
+
// npm package). Two entry points reach the package's executor:
|
|
3
|
+
//
|
|
4
|
+
// - the tool's actions (getGuide / getSchema / checkScript / present), posted
|
|
5
|
+
// by the MCP bridge to API_ROUTES.kentei.dispatch (server/api/routes/plugins.ts);
|
|
6
|
+
// - the View's `useRuntime().dispatch({ kind: "grade", … })`, registered here
|
|
7
|
+
// as the built-in "kentei" dispatch handler so it resolves on
|
|
8
|
+
// /api/plugins/runtime/kentei/dispatch.
|
|
9
|
+
//
|
|
10
|
+
// Both hand the package the same context. Its only capability is
|
|
11
|
+
// `files.byPath` (`.json` only), which is what makes the tool's `path`
|
|
12
|
+
// argument work — a runtime-plugin install gets an empty context
|
|
13
|
+
// (server/api/routes/runtime-plugin.ts), so `path` would be refused there.
|
|
14
|
+
// Grading reads TYPESAFE_API_KEY (and optional JEV_MODEL / JEV_URL) from this
|
|
15
|
+
// process's environment inside the package; the key never reaches the browser.
|
|
16
|
+
// Imported for side effect at boot (server/index.ts).
|
|
17
|
+
|
|
18
|
+
import { isGradeRequest, manageKenteiScript, type KenteiContext } from "kenteiscript/plugin";
|
|
19
|
+
import { describeKind, registerBuiltinDispatch } from "./builtin-dispatch.js";
|
|
20
|
+
import { makeByPathFileOps } from "../utils/files/by-path.js";
|
|
21
|
+
|
|
22
|
+
/** Scope name — matches `wrapWithScope("kentei", …)` in
|
|
23
|
+
* `src/plugins/manageKenteiScript/index.ts`, which is what the View's
|
|
24
|
+
* `useRuntime().dispatch` uses as the `:pkg` path segment. */
|
|
25
|
+
const KENTEI_SCOPE = "kentei";
|
|
26
|
+
|
|
27
|
+
/** A KenteiScript is a JSON file, so `path` may name only `.json`. */
|
|
28
|
+
export const KENTEI_SCRIPT_EXTENSIONS: readonly string[] = [".json"];
|
|
29
|
+
|
|
30
|
+
// Built once: by-path file ops hold no state.
|
|
31
|
+
const kenteiFiles = { byPath: makeByPathFileOps(KENTEI_SCRIPT_EXTENSIONS) };
|
|
32
|
+
|
|
33
|
+
/** Run the package's executor with MulmoClaude's capabilities added to
|
|
34
|
+
* whatever per-request context the caller has (e.g. `currentResult`). */
|
|
35
|
+
export function executeKenteiScript(context: KenteiContext, args: unknown): Promise<unknown> {
|
|
36
|
+
return manageKenteiScript({ ...context, files: kenteiFiles }, args);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// `args` is whatever the View put on the wire. The View dispatches only
|
|
40
|
+
// `{ kind: "grade" }`; tool actions arrive on the MCP route, so anything else
|
|
41
|
+
// here is refused rather than run.
|
|
42
|
+
registerBuiltinDispatch(KENTEI_SCOPE, async (args) => {
|
|
43
|
+
if (!isGradeRequest(args)) {
|
|
44
|
+
throw new Error(`kentei plugin: unrecognised dispatch payload (kind=${describeKind(args)})`);
|
|
45
|
+
}
|
|
46
|
+
return executeKenteiScript({}, args);
|
|
47
|
+
});
|
package/src/config/roles.ts
CHANGED
|
@@ -239,11 +239,12 @@ export const ROLES: [Role, ...Role[]] = [
|
|
|
239
239
|
name: "Tutor",
|
|
240
240
|
icon: "school",
|
|
241
241
|
prompt:
|
|
242
|
-
"You are an experienced tutor who adapts to each student's level. Before teaching any topic, you MUST first evaluate the student's current knowledge
|
|
242
|
+
"You are an experienced tutor who adapts to each student's level. Before teaching any topic, you MUST first evaluate the student's current knowledge with a short quiz of 4-5 questions about the topic. If you have the manageKenteiScript tool, use it for this quiz (the student answers in their own words and each answer is graded); only if you do not have it, ask the questions with putQuestions instead. If manageKenteiScript returns an error, or the quiz results say its answers couldn't be graded, ask the questions again with putQuestions rather than leaving the student without an assessment. Based on the results, adjust your teaching approach to match their understanding level. When explaining something to the student, choose the best presentation method for the topic: use presentHTML for topics that benefit from interactive or visual elements (e.g. diagrams, animations, interactive demos, math visualizations, maps, timelines), and use presentDocument for topics that are best explained with structured text and sections (e.g. definitions, historical facts, step-by-step processes). Use generateImage to create visual aids when appropriate. Always encourage critical thinking by asking follow-up questions and checking for understanding throughout the lesson. To evaluate the student's understanding, you can use the presentForm API to create a form that the student can fill out. For a practice test after a lesson, use manageKenteiScript the same way (or putQuestions if you do not have it). With manageKenteiScript, call getGuide, then write the KenteiScript and call present with it, in one go without replying to the student in between; if present lists problems, fix them and call present again; then wait for the results the student sends.",
|
|
243
243
|
availablePlugins: [
|
|
244
244
|
TOOL_NAMES.putQuestions,
|
|
245
245
|
TOOL_NAMES.presentDocument,
|
|
246
246
|
TOOL_NAMES.presentForm,
|
|
247
|
+
TOOL_NAMES.manageKenteiScript,
|
|
247
248
|
TOOL_NAMES.generateImage,
|
|
248
249
|
TOOL_NAMES.presentHtml,
|
|
249
250
|
TOOL_NAMES.presentChart,
|
package/src/main.ts
CHANGED
|
@@ -60,6 +60,7 @@ const pluginEndpointRegistry: EndpointRegistry = {
|
|
|
60
60
|
html: API_ROUTES.html,
|
|
61
61
|
svg: API_ROUTES.svg,
|
|
62
62
|
shapescript: API_ROUTES.shapescript,
|
|
63
|
+
kentei: API_ROUTES.kentei,
|
|
63
64
|
chart: API_ROUTES.chart,
|
|
64
65
|
accounting: API_ROUTES.accounting,
|
|
65
66
|
canvas: API_ROUTES.canvas,
|
|
@@ -8,6 +8,7 @@ import { META as canvasMeta } from "../canvas/meta";
|
|
|
8
8
|
import { META as chartMeta } from "../chart/meta";
|
|
9
9
|
import { META as editImagesMeta } from "../editImages/meta";
|
|
10
10
|
import { META as generateImageMeta } from "../generateImage/meta";
|
|
11
|
+
import { META as manageKenteiScriptMeta } from "../manageKenteiScript/meta";
|
|
11
12
|
import { META as manageSkillsMeta } from "../manageSkills/meta";
|
|
12
13
|
import { META as markdownMeta } from "../markdown/meta";
|
|
13
14
|
import { META as photoLocationsMeta } from "../photoLocations/meta";
|
|
@@ -29,6 +30,7 @@ export const GENERATED_PLUGIN_METAS = [
|
|
|
29
30
|
chartMeta,
|
|
30
31
|
editImagesMeta,
|
|
31
32
|
generateImageMeta,
|
|
33
|
+
manageKenteiScriptMeta,
|
|
32
34
|
manageSkillsMeta,
|
|
33
35
|
markdownMeta,
|
|
34
36
|
photoLocationsMeta,
|
|
@@ -8,6 +8,7 @@ import { REGISTRATION as canvasRegistration } from "../canvas";
|
|
|
8
8
|
import { REGISTRATION as chartRegistration } from "../chart";
|
|
9
9
|
import { REGISTRATION as editImagesRegistration } from "../editImages";
|
|
10
10
|
import { REGISTRATION as generateImageRegistration } from "../generateImage";
|
|
11
|
+
import { REGISTRATION as manageKenteiScriptRegistration } from "../manageKenteiScript";
|
|
11
12
|
import { REGISTRATION as manageSkillsRegistration } from "../manageSkills";
|
|
12
13
|
import { REGISTRATION as markdownRegistration } from "../markdown";
|
|
13
14
|
import { REGISTRATION as photoLocationsRegistration } from "../photoLocations";
|
|
@@ -33,6 +34,7 @@ export const GENERATED_PLUGIN_REGISTRATIONS: readonly PluginRegistration[] = [
|
|
|
33
34
|
chartRegistration,
|
|
34
35
|
editImagesRegistration,
|
|
35
36
|
generateImageRegistration,
|
|
37
|
+
manageKenteiScriptRegistration,
|
|
36
38
|
manageSkillsRegistration,
|
|
37
39
|
markdownRegistration,
|
|
38
40
|
photoLocationsRegistration,
|
|
@@ -7,6 +7,7 @@ import { mcpBinding } from "../server-bindings-types";
|
|
|
7
7
|
import accountingDef from "../accounting/definition";
|
|
8
8
|
import canvasDef from "../canvas/definition";
|
|
9
9
|
import chartDef from "../chart/definition";
|
|
10
|
+
import manageKenteiScriptDef from "../manageKenteiScript/definition";
|
|
10
11
|
import manageSkillsDef from "../manageSkills/definition";
|
|
11
12
|
import markdownDef from "../markdown/definition";
|
|
12
13
|
import photoLocationsDef from "../photoLocations/definition";
|
|
@@ -21,6 +22,7 @@ import spreadsheetDef from "../spreadsheet/definition";
|
|
|
21
22
|
import { META as accountingMeta } from "../accounting/meta";
|
|
22
23
|
import { META as canvasMeta } from "../canvas/meta";
|
|
23
24
|
import { META as chartMeta } from "../chart/meta";
|
|
25
|
+
import { META as manageKenteiScriptMeta } from "../manageKenteiScript/meta";
|
|
24
26
|
import { META as manageSkillsMeta } from "../manageSkills/meta";
|
|
25
27
|
import { META as markdownMeta } from "../markdown/meta";
|
|
26
28
|
import { META as photoLocationsMeta } from "../photoLocations/meta";
|
|
@@ -40,6 +42,7 @@ export const GENERATED_SERVER_BINDINGS: readonly ServerPluginBinding[] = [
|
|
|
40
42
|
mcpBinding(accountingDef, accountingMeta),
|
|
41
43
|
mcpBinding(canvasDef, canvasMeta),
|
|
42
44
|
mcpBinding(chartDef, chartMeta),
|
|
45
|
+
mcpBinding(manageKenteiScriptDef, manageKenteiScriptMeta),
|
|
43
46
|
mcpBinding(manageSkillsDef, manageSkillsMeta),
|
|
44
47
|
mcpBinding(markdownDef, markdownMeta),
|
|
45
48
|
mcpBinding(photoLocationsDef, photoLocationsMeta),
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { META } from "./meta";
|
|
2
|
+
import type { ResolvedRoute } from "../meta-types";
|
|
3
|
+
import { TOOL_DEFINITION } from "kenteiscript/plugin";
|
|
4
|
+
|
|
5
|
+
// manageKenteiScript's tool schema, KenteiScript validation, JEV grading, View
|
|
6
|
+
// and Preview live in the `kenteiscript` npm package (also used by MulmoTerminal
|
|
7
|
+
// and MulmoChat). This built-in is a thin host adapter, like presentShapeScript:
|
|
8
|
+
// it keeps MulmoClaude's routing META and scoped-runtime wrapping, and exists
|
|
9
|
+
// (rather than a runtime plugin) so the server can hand the package
|
|
10
|
+
// `files.byPath`, which is what makes the tool's `path` argument work.
|
|
11
|
+
//
|
|
12
|
+
// Server-only: this imports the package's server entry. The browser side
|
|
13
|
+
// (index.ts) takes the definition from `kenteiscript/plugin/vue`.
|
|
14
|
+
export const TOOL_NAME = META.toolName;
|
|
15
|
+
|
|
16
|
+
/** Resolved-URL view of the plugin's routes (dispatch). Auto-derived from META. */
|
|
17
|
+
export type KenteiScriptEndpoints = { readonly [K in keyof typeof META.apiRoutes]: ResolvedRoute };
|
|
18
|
+
|
|
19
|
+
export { TOOL_DEFINITION };
|
|
20
|
+
export default TOOL_DEFINITION;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import type { ToolPlugin } from "gui-chat-protocol/vue";
|
|
2
|
+
import type { PluginRegistration } from "../../tools/types";
|
|
3
|
+
import { plugin as kenteiPlugin, type KenteiData, type KenteiJson } from "kenteiscript/plugin/vue";
|
|
4
|
+
// The View's scoped styles are a standalone stylesheet; Vite lib mode does not
|
|
5
|
+
// inject it, so the consumer imports it (same as presentShapeScript / presentForm).
|
|
6
|
+
import "kenteiscript/plugin/style.css";
|
|
7
|
+
import type { KenteiScriptEndpoints } from "./definition";
|
|
8
|
+
import { META } from "./meta";
|
|
9
|
+
import { makeRouteExecute } from "../execute";
|
|
10
|
+
import { wrapWithScope } from "../scope";
|
|
11
|
+
|
|
12
|
+
// Scope name: the View's `useRuntime().dispatch({ kind: "grade", … })` posts to
|
|
13
|
+
// /api/plugins/runtime/kentei/dispatch, answered by the built-in dispatch handler
|
|
14
|
+
// registered in server/plugins/kentei-builtin.ts (JEV grading with the server's
|
|
15
|
+
// TYPESAFE_API_KEY — the key never reaches the browser).
|
|
16
|
+
const KENTEI_SCOPE = "kentei";
|
|
17
|
+
|
|
18
|
+
const TOOL_NAME = META.toolName;
|
|
19
|
+
|
|
20
|
+
// Imported from the browser entry, not ./definition, so the server bundle (Zod
|
|
21
|
+
// and the grading code) stays out of the client.
|
|
22
|
+
const pkg: ToolPlugin<KenteiData, KenteiJson, object> = kenteiPlugin;
|
|
23
|
+
|
|
24
|
+
// `unknown` JSON slot: the route executor returns the server body as-is, so it
|
|
25
|
+
// cannot vouch for `jsonData`'s shape (the View reads `data`, not `jsonData`).
|
|
26
|
+
const kenteiScriptPlugin: ToolPlugin<KenteiData, unknown, object> = {
|
|
27
|
+
...pkg,
|
|
28
|
+
// Every action runs on the server (POST /api/kentei), which owns the key
|
|
29
|
+
// check and the `files.byPath` capability for `path`.
|
|
30
|
+
execute: makeRouteExecute<KenteiScriptEndpoints, KenteiData>(KENTEI_SCOPE, "dispatch", TOOL_NAME),
|
|
31
|
+
...(pkg.viewComponent ? { viewComponent: wrapWithScope(KENTEI_SCOPE, pkg.viewComponent) } : {}),
|
|
32
|
+
...(pkg.previewComponent ? { previewComponent: wrapWithScope(KENTEI_SCOPE, pkg.previewComponent) } : {}),
|
|
33
|
+
};
|
|
34
|
+
export { TOOL_NAME };
|
|
35
|
+
|
|
36
|
+
export const REGISTRATION: PluginRegistration = {
|
|
37
|
+
toolName: TOOL_NAME,
|
|
38
|
+
entry: kenteiScriptPlugin,
|
|
39
|
+
};
|
|
@@ -0,0 +1,13 @@
|
|
|
1
|
+
import { definePluginMeta } from "../meta-types";
|
|
2
|
+
|
|
3
|
+
export const META = definePluginMeta({
|
|
4
|
+
toolName: "manageKenteiScript",
|
|
5
|
+
apiNamespace: "kentei",
|
|
6
|
+
apiRoutes: {
|
|
7
|
+
/** POST /api/kentei — a manageKenteiScript action (getGuide / getSchema /
|
|
8
|
+
* checkScript / present). The View's `{ kind: "grade" }` calls go through
|
|
9
|
+
* the built-in dispatch channel instead (server/plugins/kentei-builtin.ts). */
|
|
10
|
+
dispatch: { method: "POST", path: "" },
|
|
11
|
+
},
|
|
12
|
+
mcpDispatch: "dispatch",
|
|
13
|
+
});
|
|
@@ -11,6 +11,9 @@
|
|
|
11
11
|
*/
|
|
12
12
|
|
|
13
13
|
import type { SpreadsheetCell } from "./types.js";
|
|
14
|
+
import { isCallableName } from "./registry.js";
|
|
15
|
+
// Registers the functions, so isCallableName knows LOG10 before any sheet is calculated.
|
|
16
|
+
import "./functions/index.js";
|
|
14
17
|
|
|
15
18
|
/** Inputs to the cell builder. Mirrors the mini editor refs in the
|
|
16
19
|
* View but as plain values so unit tests don't need a Vue runtime. */
|
|
@@ -29,7 +32,12 @@ export interface MiniEditorInput {
|
|
|
29
32
|
// Anchored at the start of the input (after optional unary +/-) so we
|
|
30
33
|
// only treat expressions that clearly begin with a function call as
|
|
31
34
|
// formulas. Unanchored would match "abc FOO(" inside ordinary text.
|
|
32
|
-
const FORMULA_FUNCTION_CALL = /^[-+]?\s*[A-Z]
|
|
35
|
+
const FORMULA_FUNCTION_CALL = /^[-+]?\s*([A-Z][A-Z0-9]*)\s*\(/i;
|
|
36
|
+
|
|
37
|
+
const startsWithFunctionCall = (input: string): boolean => {
|
|
38
|
+
const name = input.match(FORMULA_FUNCTION_CALL)?.[1];
|
|
39
|
+
return name !== undefined && isCallableName(name);
|
|
40
|
+
};
|
|
33
41
|
|
|
34
42
|
// `A1 + B2` style — cell reference next to an arithmetic operator.
|
|
35
43
|
const FORMULA_CELL_OP = /[A-Z]+\d+\s*[+\-*/^]/;
|
|
@@ -49,7 +57,7 @@ const STRICT_NUMBER = /^[-+]?(?:\d+\.?\d*|\.\d+)(?:[eE][-+]?\d+)?$/;
|
|
|
49
57
|
* formula and gets the "=" prefix the engine expects.
|
|
50
58
|
*/
|
|
51
59
|
export function looksLikeFormula(input: string): boolean {
|
|
52
|
-
return
|
|
60
|
+
return startsWithFunctionCall(input) || FORMULA_CELL_OP.test(input) || FORMULA_NUMERIC_OP.test(input);
|
|
53
61
|
}
|
|
54
62
|
|
|
55
63
|
/**
|
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Evaluates spreadsheet formulas including functions, cell references, and arithmetic
|
|
5
5
|
*/
|
|
6
6
|
|
|
7
|
-
import { functionRegistry, tooFewArgumentsError } from "./registry";
|
|
7
|
+
import { functionRegistry, isCallableName, tooFewArgumentsError } from "./registry";
|
|
8
8
|
import type { CellValue } from "./types";
|
|
9
9
|
import { parseDate } from "./date-parser";
|
|
10
10
|
import { caretToPow, replaceConcatOperator, rewriteComparisonEq, isSafeArithmetic, isSafeComparison } from "./translateFormula";
|
|
@@ -281,8 +281,8 @@ export function evaluateFormula(formula: string, context: EvaluatorContext): Cel
|
|
|
281
281
|
|
|
282
282
|
// Check if it's a SIMPLE function call (not a complex expression)
|
|
283
283
|
// We need to ensure the formula is JUST a function, not "FUNC(...) + something"
|
|
284
|
-
const [, funcName, argsStr] = formula.match(/^([A-Z]
|
|
285
|
-
if (funcName !== undefined && argsStr !== undefined) {
|
|
284
|
+
const [, funcName, argsStr] = formula.match(/^([A-Z][A-Z0-9]*)\((.*)\)$/i) ?? [];
|
|
285
|
+
if (funcName !== undefined && argsStr !== undefined && isCallableName(funcName)) {
|
|
286
286
|
// Check that the closing paren is actually the end of the function
|
|
287
287
|
// by counting parentheses in argsStr
|
|
288
288
|
let parenDepth = 0;
|
|
@@ -346,8 +346,13 @@ export function evaluateFormula(formula: string, context: EvaluatorContext): Cel
|
|
|
346
346
|
|
|
347
347
|
while (searchIndex < expr.length && iterations < maxIterations) {
|
|
348
348
|
iterations++;
|
|
349
|
-
|
|
350
|
-
if (
|
|
349
|
+
// A call written inside a string literal is text, not a call.
|
|
350
|
+
if (expr[searchIndex] === '"' || expr[searchIndex] === "'") {
|
|
351
|
+
searchIndex = endOfStringLiteral(expr, searchIndex);
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
354
|
+
const [, funcName] = expr.substring(searchIndex).match(/^([A-Z][A-Z0-9]*)\(/i) ?? [];
|
|
355
|
+
if (funcName === undefined || !isCallableName(funcName)) {
|
|
351
356
|
// No more functions found, move to next character
|
|
352
357
|
searchIndex++;
|
|
353
358
|
if (searchIndex >= expr.length) break;
|
|
@@ -101,6 +101,11 @@ class FunctionRegistry {
|
|
|
101
101
|
|
|
102
102
|
export const functionRegistry = new FunctionRegistry();
|
|
103
103
|
|
|
104
|
+
/** Whether `name` written before "(" is a function call. A letters-only name is,
|
|
105
|
+
* registered or not (an unknown one reports #NAME?). A name with digits is only
|
|
106
|
+
* when registered: LOG10 is also shaped like a cell reference, so `A1(2)` is not. */
|
|
107
|
+
export const isCallableName = (name: string): boolean => /^[A-Z]+$/i.test(name) || functionRegistry.hasFunction(name);
|
|
108
|
+
|
|
104
109
|
/**
|
|
105
110
|
* Lenient numeric coercion for range aggregation: anything unreadable is 0.
|
|
106
111
|
* PINNED behaviour (booleans are 0, not Excel's 1/0) — the string parsing lives
|