logisheets-logician 1.12.0 → 1.13.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/dist/index.node.js +128 -23
- package/dist/tools/builder.d.ts +9 -0
- package/dist/tools/builder.js +155 -18
- package/dist/tools/cells.js +4 -1
- package/dist/tools/edit.d.ts +8 -5
- package/dist/tools/edit.js +15 -10
- package/package.json +3 -3
package/dist/index.node.js
CHANGED
|
@@ -807,6 +807,22 @@ function ensureOk(effect, label) {
|
|
|
807
807
|
throw transactionFailure(label, effect);
|
|
808
808
|
}
|
|
809
809
|
}
|
|
810
|
+
function cellDisplay(c) {
|
|
811
|
+
const v = c?.value;
|
|
812
|
+
if (v === void 0 || v === "empty") return "";
|
|
813
|
+
if (typeof v === "object" && "value" in v) {
|
|
814
|
+
return String(v.value);
|
|
815
|
+
}
|
|
816
|
+
return "";
|
|
817
|
+
}
|
|
818
|
+
function cellTextOnly(c) {
|
|
819
|
+
const v = c?.value;
|
|
820
|
+
if (v === void 0 || v === "empty") return "";
|
|
821
|
+
if (typeof v === "object" && "type" in v && v.type === "str") {
|
|
822
|
+
return String(v.value);
|
|
823
|
+
}
|
|
824
|
+
return "";
|
|
825
|
+
}
|
|
810
826
|
function stringifyForBlockInput(v) {
|
|
811
827
|
if (v === null || v === void 0) return "";
|
|
812
828
|
if (typeof v === "string") return v;
|
|
@@ -2412,7 +2428,9 @@ var convertToBlock = {
|
|
|
2412
2428
|
"",
|
|
2413
2429
|
"This is how you adopt a workbook someone hands you. `create_block` is for new tables and refuses to write over existing data; this one takes the data as it stands and gives it a name, fields and row keys, so you can address it as (block, row_key, field) and reference it from formulas by name instead of by coordinate.",
|
|
2414
2430
|
"",
|
|
2415
|
-
"Give `position` and the counts for the DATA only, leaving out any header row
|
|
2431
|
+
"Give `position` and the counts for the DATA only, leaving out any header row. Field names come from the header row \u2014 pass `header_row`, or leave it out and the row directly above the data is used when it looks like titles. `fields` states them outright instead.",
|
|
2432
|
+
"",
|
|
2433
|
+
"The row key is inferred: the first column whose values are all present and all different. Pass `key_field` to choose. Whatever is inferred is reported back, so check it \u2014 the key is how formulas and later edits name a row.",
|
|
2416
2434
|
"",
|
|
2417
2435
|
"Converting is a one-time cost: the block and its schema survive saving and reloading, and afterwards the region behaves exactly like one created as a block."
|
|
2418
2436
|
].join("\n"),
|
|
@@ -2439,7 +2457,11 @@ var convertToBlock = {
|
|
|
2439
2457
|
header_row: {
|
|
2440
2458
|
type: "integer",
|
|
2441
2459
|
minimum: 0,
|
|
2442
|
-
description: "Row holding the column titles; field names are read from it.
|
|
2460
|
+
description: "Row holding the column titles; field names are read from it. Omit it and the row directly above `position` is used when it looks like a header \u2014 all text, over data that is not."
|
|
2461
|
+
},
|
|
2462
|
+
key_field: {
|
|
2463
|
+
type: "string",
|
|
2464
|
+
description: "Which field identifies a row. Omit and the first column whose values are all present and all different is used."
|
|
2443
2465
|
},
|
|
2444
2466
|
fields: {
|
|
2445
2467
|
type: "array",
|
|
@@ -2482,29 +2504,77 @@ var convertToBlock = {
|
|
|
2482
2504
|
);
|
|
2483
2505
|
}
|
|
2484
2506
|
}
|
|
2507
|
+
const width = input.col_count;
|
|
2508
|
+
const dataRes = await client.getCells({
|
|
2509
|
+
sheetIdx,
|
|
2510
|
+
startRow: row,
|
|
2511
|
+
startCol: col,
|
|
2512
|
+
endRow,
|
|
2513
|
+
endCol
|
|
2514
|
+
});
|
|
2515
|
+
if (isErrorMessage(dataRes)) {
|
|
2516
|
+
throw new Error(`reading the table failed: ${dataRes.msg}`);
|
|
2517
|
+
}
|
|
2518
|
+
const data = dataRes;
|
|
2519
|
+
const at = (r, c) => data[r * width + c];
|
|
2520
|
+
const anyData = data.some((c) => cellDisplay(c) !== "" || c.formula);
|
|
2521
|
+
if (!anyData) {
|
|
2522
|
+
throw new Error(
|
|
2523
|
+
`there is nothing in the region at row ${row}, col ${col} (${input.row_count}x${input.col_count}) \u2014 this adopts a table that already exists; use create_block for a new one`
|
|
2524
|
+
);
|
|
2525
|
+
}
|
|
2526
|
+
let headerRow = input.header_row;
|
|
2527
|
+
let headerSource = "stated";
|
|
2528
|
+
let headerDeclined = "there is no row above the data";
|
|
2529
|
+
if (headerRow === void 0 && input.fields === void 0 && row > 0) {
|
|
2530
|
+
const above = await client.getCells({
|
|
2531
|
+
sheetIdx,
|
|
2532
|
+
startRow: row - 1,
|
|
2533
|
+
startCol: col,
|
|
2534
|
+
endRow: row - 1,
|
|
2535
|
+
endCol
|
|
2536
|
+
});
|
|
2537
|
+
if (!isErrorMessage(above)) {
|
|
2538
|
+
const titles = above;
|
|
2539
|
+
const allText = titles.length === width && titles.every((c) => cellTextOnly(c).trim() !== "");
|
|
2540
|
+
const dataAllText = Array.from(
|
|
2541
|
+
{ length: width },
|
|
2542
|
+
(_, i) => cellTextOnly(at(0, i))
|
|
2543
|
+
).every((t) => t.trim() !== "");
|
|
2544
|
+
if (allText && !dataAllText) {
|
|
2545
|
+
headerRow = row - 1;
|
|
2546
|
+
headerSource = "inferred from the row above the data";
|
|
2547
|
+
} else if (allText && dataAllText) {
|
|
2548
|
+
headerDeclined = `the row above is all text but so is the first data row, so which one is titles cannot be told apart \u2014 pass \`header_row\` (probably ${row - 1}) or \`fields\``;
|
|
2549
|
+
} else {
|
|
2550
|
+
headerDeclined = "the row above the data is not all text";
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
}
|
|
2485
2554
|
let fields;
|
|
2555
|
+
let fieldSource;
|
|
2486
2556
|
if (input.fields !== void 0) {
|
|
2487
2557
|
fields = [...input.fields];
|
|
2488
|
-
|
|
2558
|
+
fieldSource = "stated";
|
|
2559
|
+
} else if (headerRow !== void 0) {
|
|
2489
2560
|
const cells = await client.getCells({
|
|
2490
2561
|
sheetIdx,
|
|
2491
|
-
startRow:
|
|
2562
|
+
startRow: headerRow,
|
|
2492
2563
|
startCol: col,
|
|
2493
|
-
endRow:
|
|
2564
|
+
endRow: headerRow,
|
|
2494
2565
|
endCol
|
|
2495
2566
|
});
|
|
2496
2567
|
if (isErrorMessage(cells)) {
|
|
2497
2568
|
throw new Error(`reading the header row failed: ${cells.msg}`);
|
|
2498
2569
|
}
|
|
2499
2570
|
fields = cells.map((c, i) => {
|
|
2500
|
-
const
|
|
2501
|
-
|
|
2502
|
-
return text.trim() === "" ? `field_${i + 1}` : text.trim();
|
|
2571
|
+
const text = cellDisplay(c).trim();
|
|
2572
|
+
return text === "" ? `field_${i + 1}` : text;
|
|
2503
2573
|
});
|
|
2574
|
+
fieldSource = `read from row ${headerRow} (${headerSource})`;
|
|
2504
2575
|
} else {
|
|
2505
|
-
|
|
2506
|
-
|
|
2507
|
-
);
|
|
2576
|
+
fields = Array.from({ length: width }, (_, i) => `field_${i + 1}`);
|
|
2577
|
+
fieldSource = `generated, because ${headerDeclined}`;
|
|
2508
2578
|
}
|
|
2509
2579
|
if (fields.length !== input.col_count) {
|
|
2510
2580
|
throw new Error(
|
|
@@ -2517,6 +2587,32 @@ var convertToBlock = {
|
|
|
2517
2587
|
`duplicate field name(s) ${[...new Set(dup)].map((d) => `"${d}"`).join(", ")} \u2014 fields address cells, so they must be unique. Pass \`fields\` explicitly to disambiguate.`
|
|
2518
2588
|
);
|
|
2519
2589
|
}
|
|
2590
|
+
const columnCanKey = (c) => {
|
|
2591
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2592
|
+
for (let r = 0; r < input.row_count; r++) {
|
|
2593
|
+
const text = cellDisplay(at(r, c)).trim();
|
|
2594
|
+
if (text === "" || seen.has(text)) return false;
|
|
2595
|
+
seen.add(text);
|
|
2596
|
+
}
|
|
2597
|
+
return true;
|
|
2598
|
+
};
|
|
2599
|
+
let keyIdx;
|
|
2600
|
+
let keySource;
|
|
2601
|
+
if (input.key_field !== void 0) {
|
|
2602
|
+
keyIdx = fields.indexOf(input.key_field);
|
|
2603
|
+
if (keyIdx < 0) {
|
|
2604
|
+
throw new Error(
|
|
2605
|
+
`key_field "${input.key_field}" is not one of the fields ${fields.map((f) => `"${f}"`).join(", ")}`
|
|
2606
|
+
);
|
|
2607
|
+
}
|
|
2608
|
+
keySource = "stated";
|
|
2609
|
+
} else {
|
|
2610
|
+
const found = Array.from({ length: width }, (_, i) => i).find(
|
|
2611
|
+
columnCanKey
|
|
2612
|
+
);
|
|
2613
|
+
keyIdx = found ?? 0;
|
|
2614
|
+
keySource = found === void 0 ? "fell back to the first column \u2014 no column has a value in every row that is also unique" : "inferred: its values are all present and all different";
|
|
2615
|
+
}
|
|
2520
2616
|
const idRes = await client.getAvailableBlockId({ sheetIdx });
|
|
2521
2617
|
if (isErrorMessage(idRes)) {
|
|
2522
2618
|
throw new Error(`getAvailableBlockId failed: ${idRes.msg}`);
|
|
@@ -2531,14 +2627,21 @@ var convertToBlock = {
|
|
|
2531
2627
|
},
|
|
2532
2628
|
{
|
|
2533
2629
|
type: "bindFormSchema",
|
|
2534
|
-
value: new BindFormSchemaBuilder().refName(input.name).sheetIdx(sheetIdx).blockId(blockId).fieldFrom(0).keyIdx(
|
|
2630
|
+
value: new BindFormSchemaBuilder().refName(input.name).sheetIdx(sheetIdx).blockId(blockId).fieldFrom(0).keyIdx(keyIdx).fields(fields).renderIds(fields.map((_, i) => `${input.name}__f${i}`)).fieldFormulas([]).validationFormulas([]).editabilityFormulas([]).row(true).build()
|
|
2535
2631
|
}
|
|
2536
2632
|
],
|
|
2537
2633
|
`convert_to_block("${input.name}")`
|
|
2538
2634
|
);
|
|
2539
2635
|
return {
|
|
2540
|
-
data: {
|
|
2541
|
-
|
|
2636
|
+
data: {
|
|
2637
|
+
block_id: blockId,
|
|
2638
|
+
fields,
|
|
2639
|
+
key_field: fields[keyIdx],
|
|
2640
|
+
header_row: headerRow ?? null
|
|
2641
|
+
},
|
|
2642
|
+
display: `Converted the table at row ${row}, col ${col} into block "${input.name}" (${input.row_count} row(s)). Values untouched.
|
|
2643
|
+
Fields: ${fields.map((f) => `"${f}"`).join(", ")} \u2014 ${fieldSource}.
|
|
2644
|
+
Row key: "${fields[keyIdx]}" \u2014 ${keySource}.`
|
|
2542
2645
|
};
|
|
2543
2646
|
}
|
|
2544
2647
|
};
|
|
@@ -2897,7 +3000,7 @@ var previewChanges = {
|
|
|
2897
3000
|
throw transactionFailure(`preview_changes (${where})`, result);
|
|
2898
3001
|
}
|
|
2899
3002
|
if (watchCoords.length > 0) {
|
|
2900
|
-
const
|
|
3003
|
+
const values = [];
|
|
2901
3004
|
for (const w of watchCoords) {
|
|
2902
3005
|
const cells = await client.getCells({
|
|
2903
3006
|
sheetIdx: w.sheetIdx,
|
|
@@ -2910,12 +3013,11 @@ var previewChanges = {
|
|
|
2910
3013
|
throw new Error(`getCells failed: ${cells.msg}`);
|
|
2911
3014
|
}
|
|
2912
3015
|
const info = cells[0];
|
|
2913
|
-
|
|
2914
|
-
|
|
2915
|
-
|
|
2916
|
-
});
|
|
3016
|
+
values.push(
|
|
3017
|
+
info === void 0 ? null : flattenValue2(info.value)
|
|
3018
|
+
);
|
|
2917
3019
|
}
|
|
2918
|
-
results.push({ label: scenario.label,
|
|
3020
|
+
results.push({ label: scenario.label, values });
|
|
2919
3021
|
} else {
|
|
2920
3022
|
const diffRes = await client.getTempStatusChanges();
|
|
2921
3023
|
if (isErrorMessage(diffRes)) {
|
|
@@ -2965,7 +3067,10 @@ var previewChanges = {
|
|
|
2965
3067
|
return `${results.length} scenarios previewed.`;
|
|
2966
3068
|
};
|
|
2967
3069
|
return {
|
|
2968
|
-
data: single !== void 0 ? { scenarios: results, diff: single } :
|
|
3070
|
+
data: single !== void 0 ? { scenarios: results, diff: single } : watchCoords.length > 0 ? {
|
|
3071
|
+
watching: watchCoords.map((w) => w.target),
|
|
3072
|
+
scenarios: results
|
|
3073
|
+
} : { scenarios: results },
|
|
2969
3074
|
display: describe()
|
|
2970
3075
|
};
|
|
2971
3076
|
}
|
|
@@ -2980,7 +3085,7 @@ function flattenValue2(v) {
|
|
|
2980
3085
|
case "bool":
|
|
2981
3086
|
return v.value;
|
|
2982
3087
|
case "error":
|
|
2983
|
-
return
|
|
3088
|
+
return v.value;
|
|
2984
3089
|
}
|
|
2985
3090
|
}
|
|
2986
3091
|
function locateInBlock(sheetIdx, row, col, blocks) {
|
|
@@ -3948,7 +4053,7 @@ function valueToScalar(v) {
|
|
|
3948
4053
|
case "bool":
|
|
3949
4054
|
return v.value;
|
|
3950
4055
|
case "error":
|
|
3951
|
-
return
|
|
4056
|
+
return v.value;
|
|
3952
4057
|
default:
|
|
3953
4058
|
return null;
|
|
3954
4059
|
}
|
package/dist/tools/builder.d.ts
CHANGED
|
@@ -275,10 +275,19 @@ interface ConvertToBlockInput {
|
|
|
275
275
|
header_row?: number;
|
|
276
276
|
/** Field names, when there is no header row to read them from. */
|
|
277
277
|
fields?: string[];
|
|
278
|
+
/**
|
|
279
|
+
* Which field identifies a row. Omit and it is inferred: the first column
|
|
280
|
+
* whose values are all present and all different. The key is how every
|
|
281
|
+
* formula and every later edit names a row, so getting it from the data
|
|
282
|
+
* beats defaulting to whatever column happens to be first.
|
|
283
|
+
*/
|
|
284
|
+
key_field?: string;
|
|
278
285
|
}
|
|
279
286
|
export declare const convertToBlock: Tool<ConvertToBlockInput, {
|
|
280
287
|
block_id: number;
|
|
281
288
|
fields: string[];
|
|
289
|
+
key_field: string;
|
|
290
|
+
header_row: number | null;
|
|
282
291
|
}>;
|
|
283
292
|
export declare const BUILDER_TOOLS: Tool[];
|
|
284
293
|
export {};
|
package/dist/tools/builder.js
CHANGED
|
@@ -26,6 +26,31 @@ function ensureOk(effect, label) {
|
|
|
26
26
|
throw transactionFailure(label, effect);
|
|
27
27
|
}
|
|
28
28
|
}
|
|
29
|
+
/** A cell's value as a string — number, boolean or text alike. What a row key
|
|
30
|
+
* is compared on, since a key column of years or ids is perfectly ordinary. */
|
|
31
|
+
function cellDisplay(c) {
|
|
32
|
+
const v = c === null || c === void 0 ? void 0 : c.value;
|
|
33
|
+
if (v === undefined || v === 'empty')
|
|
34
|
+
return '';
|
|
35
|
+
if (typeof v === 'object' && 'value' in v) {
|
|
36
|
+
return String(v.value);
|
|
37
|
+
}
|
|
38
|
+
return '';
|
|
39
|
+
}
|
|
40
|
+
/** A cell's value only when it is TEXT, else ''. Distinct from
|
|
41
|
+
* {@link cellDisplay} because deciding whether a row is a header turns on
|
|
42
|
+
* exactly that difference: titles are words, the data under them usually is
|
|
43
|
+
* not. Treating a number as text made every numeric first row look like a
|
|
44
|
+
* second header, and the header above it went unread. */
|
|
45
|
+
function cellTextOnly(c) {
|
|
46
|
+
const v = c === null || c === void 0 ? void 0 : c.value;
|
|
47
|
+
if (v === undefined || v === 'empty')
|
|
48
|
+
return '';
|
|
49
|
+
if (typeof v === 'object' && 'type' in v && v.type === 'str') {
|
|
50
|
+
return String(v.value);
|
|
51
|
+
}
|
|
52
|
+
return '';
|
|
53
|
+
}
|
|
29
54
|
/** Coerce arbitrary JSON values into the string form BlockInput expects.
|
|
30
55
|
* Numbers / booleans / null are stringified; strings pass through. */
|
|
31
56
|
function stringifyForBlockInput(v) {
|
|
@@ -2010,7 +2035,9 @@ export const convertToBlock = {
|
|
|
2010
2035
|
'',
|
|
2011
2036
|
"This is how you adopt a workbook someone hands you. `create_block` is for new tables and refuses to write over existing data; this one takes the data as it stands and gives it a name, fields and row keys, so you can address it as (block, row_key, field) and reference it from formulas by name instead of by coordinate.",
|
|
2012
2037
|
'',
|
|
2013
|
-
'Give `position` and the counts for the DATA only, leaving out any header row
|
|
2038
|
+
'Give `position` and the counts for the DATA only, leaving out any header row. Field names come from the header row — pass `header_row`, or leave it out and the row directly above the data is used when it looks like titles. `fields` states them outright instead.',
|
|
2039
|
+
'',
|
|
2040
|
+
'The row key is inferred: the first column whose values are all present and all different. Pass `key_field` to choose. Whatever is inferred is reported back, so check it — the key is how formulas and later edits name a row.',
|
|
2014
2041
|
'',
|
|
2015
2042
|
'Converting is a one-time cost: the block and its schema survive saving and reloading, and afterwards the region behaves exactly like one created as a block.',
|
|
2016
2043
|
].join('\n'),
|
|
@@ -2037,7 +2064,11 @@ export const convertToBlock = {
|
|
|
2037
2064
|
header_row: {
|
|
2038
2065
|
type: 'integer',
|
|
2039
2066
|
minimum: 0,
|
|
2040
|
-
description: 'Row holding the column titles; field names are read from it.
|
|
2067
|
+
description: 'Row holding the column titles; field names are read from it. Omit it and the row directly above `position` is used when it looks like a header — all text, over data that is not.',
|
|
2068
|
+
},
|
|
2069
|
+
key_field: {
|
|
2070
|
+
type: 'string',
|
|
2071
|
+
description: 'Which field identifies a row. Omit and the first column whose values are all present and all different is used.',
|
|
2041
2072
|
},
|
|
2042
2073
|
fields: {
|
|
2043
2074
|
type: 'array',
|
|
@@ -2081,34 +2112,100 @@ export const convertToBlock = {
|
|
|
2081
2112
|
throw new Error(`that region overlaps the existing block "${(_b = (_a = b.schema) === null || _a === void 0 ? void 0 : _a.name) !== null && _b !== void 0 ? _b : `block#${b.blockId}`}" at row ${b.rowStart}, col ${b.colStart}`);
|
|
2082
2113
|
}
|
|
2083
2114
|
}
|
|
2084
|
-
//
|
|
2115
|
+
// The data itself, read once: the key has to be inferred from it, and
|
|
2116
|
+
// the header check wants to know whether the first data row is text.
|
|
2117
|
+
// `getCells` returns the rectangle row-major.
|
|
2118
|
+
const width = input.col_count;
|
|
2119
|
+
const dataRes = await client.getCells({
|
|
2120
|
+
sheetIdx,
|
|
2121
|
+
startRow: row,
|
|
2122
|
+
startCol: col,
|
|
2123
|
+
endRow,
|
|
2124
|
+
endCol,
|
|
2125
|
+
});
|
|
2126
|
+
if (isErrorMessage(dataRes)) {
|
|
2127
|
+
throw new Error(`reading the table failed: ${dataRes.msg}`);
|
|
2128
|
+
}
|
|
2129
|
+
const data = dataRes;
|
|
2130
|
+
const at = (r, c) => data[r * width + c];
|
|
2131
|
+
// Adopting a region with nothing in it is a mistake worth refusing.
|
|
2132
|
+
// This tool exists to take data that is already there and name it; an
|
|
2133
|
+
// empty rectangle would become a block of `field_1..n` with no rows,
|
|
2134
|
+
// which is what `create_block` is for.
|
|
2135
|
+
const anyData = data.some((c) => cellDisplay(c) !== '' || c.formula);
|
|
2136
|
+
if (!anyData) {
|
|
2137
|
+
throw new Error(`there is nothing in the region at row ${row}, col ${col} ` +
|
|
2138
|
+
`(${input.row_count}x${input.col_count}) — this adopts a table that ` +
|
|
2139
|
+
'already exists; use create_block for a new one');
|
|
2140
|
+
}
|
|
2141
|
+
// Which row holds the titles. Stated, or the row directly above the
|
|
2142
|
+
// data when it looks like titles: text all the way across, over a first
|
|
2143
|
+
// data row that is not all text. Guessing wrong here is cheap to spot —
|
|
2144
|
+
// the field names come back in the result — and the fallback is
|
|
2145
|
+
// `field_1..n`, which is obviously not a header.
|
|
2146
|
+
let headerRow = input.header_row;
|
|
2147
|
+
let headerSource = 'stated';
|
|
2148
|
+
// Why the guess was declined, when it was, so the caller knows what to
|
|
2149
|
+
// pass instead.
|
|
2150
|
+
let headerDeclined = 'there is no row above the data';
|
|
2151
|
+
if (headerRow === undefined && input.fields === undefined && row > 0) {
|
|
2152
|
+
const above = await client.getCells({
|
|
2153
|
+
sheetIdx,
|
|
2154
|
+
startRow: row - 1,
|
|
2155
|
+
startCol: col,
|
|
2156
|
+
endRow: row - 1,
|
|
2157
|
+
endCol,
|
|
2158
|
+
});
|
|
2159
|
+
if (!isErrorMessage(above)) {
|
|
2160
|
+
const titles = above;
|
|
2161
|
+
const allText = titles.length === width &&
|
|
2162
|
+
titles.every((c) => cellTextOnly(c).trim() !== '');
|
|
2163
|
+
const dataAllText = Array.from({ length: width }, (_, i) => cellTextOnly(at(0, i))).every((t) => t.trim() !== '');
|
|
2164
|
+
if (allText && !dataAllText) {
|
|
2165
|
+
headerRow = row - 1;
|
|
2166
|
+
headerSource = 'inferred from the row above the data';
|
|
2167
|
+
}
|
|
2168
|
+
else if (allText && dataAllText) {
|
|
2169
|
+
// Both are words, so there is nothing to tell them apart.
|
|
2170
|
+
// Guessing here is the one guess that could name the fields
|
|
2171
|
+
// after a row of data.
|
|
2172
|
+
headerDeclined =
|
|
2173
|
+
`the row above is all text but so is the first data row, ` +
|
|
2174
|
+
`so which one is titles cannot be told apart — pass ` +
|
|
2175
|
+
`\`header_row\` (probably ${row - 1}) or \`fields\``;
|
|
2176
|
+
}
|
|
2177
|
+
else {
|
|
2178
|
+
headerDeclined = 'the row above the data is not all text';
|
|
2179
|
+
}
|
|
2180
|
+
}
|
|
2181
|
+
}
|
|
2182
|
+
// Field names: stated, read from the header row, or generated.
|
|
2085
2183
|
let fields;
|
|
2184
|
+
let fieldSource;
|
|
2086
2185
|
if (input.fields !== undefined) {
|
|
2087
2186
|
fields = [...input.fields];
|
|
2187
|
+
fieldSource = 'stated';
|
|
2088
2188
|
}
|
|
2089
|
-
else if (
|
|
2189
|
+
else if (headerRow !== undefined) {
|
|
2090
2190
|
const cells = await client.getCells({
|
|
2091
2191
|
sheetIdx,
|
|
2092
|
-
startRow:
|
|
2192
|
+
startRow: headerRow,
|
|
2093
2193
|
startCol: col,
|
|
2094
|
-
endRow:
|
|
2194
|
+
endRow: headerRow,
|
|
2095
2195
|
endCol,
|
|
2096
2196
|
});
|
|
2097
2197
|
if (isErrorMessage(cells)) {
|
|
2098
2198
|
throw new Error(`reading the header row failed: ${cells.msg}`);
|
|
2099
2199
|
}
|
|
2100
2200
|
fields = cells.map((c, i) => {
|
|
2101
|
-
const
|
|
2102
|
-
|
|
2103
|
-
? ''
|
|
2104
|
-
: typeof v === 'object' && 'value' in v
|
|
2105
|
-
? String(v.value)
|
|
2106
|
-
: '';
|
|
2107
|
-
return text.trim() === '' ? `field_${i + 1}` : text.trim();
|
|
2201
|
+
const text = cellDisplay(c).trim();
|
|
2202
|
+
return text === '' ? `field_${i + 1}` : text;
|
|
2108
2203
|
});
|
|
2204
|
+
fieldSource = `read from row ${headerRow} (${headerSource})`;
|
|
2109
2205
|
}
|
|
2110
2206
|
else {
|
|
2111
|
-
|
|
2207
|
+
fields = Array.from({ length: width }, (_, i) => `field_${i + 1}`);
|
|
2208
|
+
fieldSource = `generated, because ${headerDeclined}`;
|
|
2112
2209
|
}
|
|
2113
2210
|
if (fields.length !== input.col_count) {
|
|
2114
2211
|
throw new Error(`got ${fields.length} field name(s) for ${input.col_count} column(s)`);
|
|
@@ -2120,6 +2217,40 @@ export const convertToBlock = {
|
|
|
2120
2217
|
.join(', ')} — fields address cells, so they must be unique. ` +
|
|
2121
2218
|
'Pass `fields` explicitly to disambiguate.');
|
|
2122
2219
|
}
|
|
2220
|
+
// Which column identifies a row. The key is how BLOCKREF names a row and
|
|
2221
|
+
// how every later edit addresses one, so it is worth deriving from the
|
|
2222
|
+
// data rather than assuming the leftmost column: the first column whose
|
|
2223
|
+
// values are all present and all different. A column with a gap cannot
|
|
2224
|
+
// address every row, and a repeat is worse than a gap — BLOCKREF would
|
|
2225
|
+
// resolve both rows to the first match.
|
|
2226
|
+
const columnCanKey = (c) => {
|
|
2227
|
+
const seen = new Set();
|
|
2228
|
+
for (let r = 0; r < input.row_count; r++) {
|
|
2229
|
+
const text = cellDisplay(at(r, c)).trim();
|
|
2230
|
+
if (text === '' || seen.has(text))
|
|
2231
|
+
return false;
|
|
2232
|
+
seen.add(text);
|
|
2233
|
+
}
|
|
2234
|
+
return true;
|
|
2235
|
+
};
|
|
2236
|
+
let keyIdx;
|
|
2237
|
+
let keySource;
|
|
2238
|
+
if (input.key_field !== undefined) {
|
|
2239
|
+
keyIdx = fields.indexOf(input.key_field);
|
|
2240
|
+
if (keyIdx < 0) {
|
|
2241
|
+
throw new Error(`key_field "${input.key_field}" is not one of the fields ` +
|
|
2242
|
+
`${fields.map((f) => `"${f}"`).join(', ')}`);
|
|
2243
|
+
}
|
|
2244
|
+
keySource = 'stated';
|
|
2245
|
+
}
|
|
2246
|
+
else {
|
|
2247
|
+
const found = Array.from({ length: width }, (_, i) => i).find(columnCanKey);
|
|
2248
|
+
keyIdx = found !== null && found !== void 0 ? found : 0;
|
|
2249
|
+
keySource =
|
|
2250
|
+
found === undefined
|
|
2251
|
+
? 'fell back to the first column — no column has a value in every row that is also unique'
|
|
2252
|
+
: 'inferred: its values are all present and all different';
|
|
2253
|
+
}
|
|
2123
2254
|
const idRes = await client.getAvailableBlockId({ sheetIdx });
|
|
2124
2255
|
if (isErrorMessage(idRes)) {
|
|
2125
2256
|
throw new Error(`getAvailableBlockId failed: ${idRes.msg}`);
|
|
@@ -2146,7 +2277,7 @@ export const convertToBlock = {
|
|
|
2146
2277
|
.sheetIdx(sheetIdx)
|
|
2147
2278
|
.blockId(blockId)
|
|
2148
2279
|
.fieldFrom(0)
|
|
2149
|
-
.keyIdx(
|
|
2280
|
+
.keyIdx(keyIdx)
|
|
2150
2281
|
.fields(fields)
|
|
2151
2282
|
.renderIds(fields.map((_, i) => `${input.name}__f${i}`))
|
|
2152
2283
|
.fieldFormulas([])
|
|
@@ -2157,10 +2288,16 @@ export const convertToBlock = {
|
|
|
2157
2288
|
},
|
|
2158
2289
|
], `convert_to_block("${input.name}")`);
|
|
2159
2290
|
return {
|
|
2160
|
-
data: {
|
|
2291
|
+
data: {
|
|
2292
|
+
block_id: blockId,
|
|
2293
|
+
fields,
|
|
2294
|
+
key_field: fields[keyIdx],
|
|
2295
|
+
header_row: headerRow !== null && headerRow !== void 0 ? headerRow : null,
|
|
2296
|
+
},
|
|
2161
2297
|
display: `Converted the table at row ${row}, col ${col} into block "${input.name}" ` +
|
|
2162
|
-
`(${input.row_count} row(s)
|
|
2163
|
-
|
|
2298
|
+
`(${input.row_count} row(s)). Values untouched.\n` +
|
|
2299
|
+
`Fields: ${fields.map((f) => `"${f}"`).join(', ')} — ${fieldSource}.\n` +
|
|
2300
|
+
`Row key: "${fields[keyIdx]}" — ${keySource}.`,
|
|
2164
2301
|
};
|
|
2165
2302
|
},
|
|
2166
2303
|
};
|
package/dist/tools/cells.js
CHANGED
|
@@ -29,7 +29,10 @@ function valueToScalar(v) {
|
|
|
29
29
|
case 'bool':
|
|
30
30
|
return v.value;
|
|
31
31
|
case 'error':
|
|
32
|
-
|
|
32
|
+
// The engine's error value already carries its `#` — `#DIV/0!`,
|
|
33
|
+
// not `DIV/0!`. Prefixing another one produced `##DIV/0!`, which
|
|
34
|
+
// matches nothing an agent or a test would look for.
|
|
35
|
+
return v.value;
|
|
33
36
|
default:
|
|
34
37
|
return null;
|
|
35
38
|
}
|
package/dist/tools/edit.d.ts
CHANGED
|
@@ -76,13 +76,16 @@ interface PreviewScenarioResult {
|
|
|
76
76
|
label?: string;
|
|
77
77
|
/** Every cell whose value changes. Present unless `watch` was given. */
|
|
78
78
|
diff?: PreviewDiffEntry[];
|
|
79
|
-
/** The watched cells' values
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
79
|
+
/** The watched cells' values, in the same order as `watching` on the
|
|
80
|
+
* result. Bare values: repeating the target on every scenario cost more
|
|
81
|
+
* bytes than all the answers put together on a 16-cell grid, and the
|
|
82
|
+
* caller named those targets in the first place. */
|
|
83
|
+
values?: CellValue[];
|
|
84
84
|
}
|
|
85
85
|
interface PreviewChangesOutput {
|
|
86
|
+
/** The watched targets, once, in the order their values appear on each
|
|
87
|
+
* scenario. Present only when `watch` was given. */
|
|
88
|
+
watching?: WatchTarget[];
|
|
86
89
|
/** One entry per scenario, in the order given. */
|
|
87
90
|
scenarios: PreviewScenarioResult[];
|
|
88
91
|
/** The single scenario's diff, when called with `changes` and no `watch` —
|
package/dist/tools/edit.js
CHANGED
|
@@ -374,7 +374,7 @@ export const previewChanges = {
|
|
|
374
374
|
// Read the watched cells inside the branch, so a cell the
|
|
375
375
|
// change did not move still reports its (unchanged) value
|
|
376
376
|
// rather than going missing from a diff.
|
|
377
|
-
const
|
|
377
|
+
const values = [];
|
|
378
378
|
for (const w of watchCoords) {
|
|
379
379
|
const cells = await client.getCells({
|
|
380
380
|
sheetIdx: w.sheetIdx,
|
|
@@ -387,14 +387,9 @@ export const previewChanges = {
|
|
|
387
387
|
throw new Error(`getCells failed: ${cells.msg}`);
|
|
388
388
|
}
|
|
389
389
|
const info = cells[0];
|
|
390
|
-
|
|
391
|
-
target: w.target,
|
|
392
|
-
value: info === undefined
|
|
393
|
-
? null
|
|
394
|
-
: flattenValue(info.value),
|
|
395
|
-
});
|
|
390
|
+
values.push(info === undefined ? null : flattenValue(info.value));
|
|
396
391
|
}
|
|
397
|
-
results.push({ label: scenario.label,
|
|
392
|
+
results.push({ label: scenario.label, values });
|
|
398
393
|
}
|
|
399
394
|
else {
|
|
400
395
|
const diffRes = await client.getTempStatusChanges();
|
|
@@ -452,7 +447,14 @@ export const previewChanges = {
|
|
|
452
447
|
return `${results.length} scenarios previewed.`;
|
|
453
448
|
};
|
|
454
449
|
return {
|
|
455
|
-
data: single !== undefined
|
|
450
|
+
data: single !== undefined
|
|
451
|
+
? { scenarios: results, diff: single }
|
|
452
|
+
: watchCoords.length > 0
|
|
453
|
+
? {
|
|
454
|
+
watching: watchCoords.map((w) => w.target),
|
|
455
|
+
scenarios: results,
|
|
456
|
+
}
|
|
457
|
+
: { scenarios: results },
|
|
456
458
|
display: describe(),
|
|
457
459
|
};
|
|
458
460
|
},
|
|
@@ -468,7 +470,10 @@ function flattenValue(v) {
|
|
|
468
470
|
case 'bool':
|
|
469
471
|
return v.value;
|
|
470
472
|
case 'error':
|
|
471
|
-
|
|
473
|
+
// The engine's error value is already spelled the way a
|
|
474
|
+
// spreadsheet spells it — `#DIV/0!`. Decorating it produced
|
|
475
|
+
// `#ERR:#DIV/0!`, which matches nothing anyone would look for.
|
|
476
|
+
return v.value;
|
|
472
477
|
}
|
|
473
478
|
}
|
|
474
479
|
/** Which (block, row_key, field) a coordinate names, if any. Shared with the
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "logisheets-logician",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.13.0",
|
|
4
4
|
"description": "AI agent tools for LogiSheets — platform-agnostic LLM tool definitions, prompts, and skills for operating a real spreadsheet engine. Powers Watson (the in-app assistant) and logisheets-mcp; reusable in any agent host.",
|
|
5
5
|
"main": "./dist/index.js",
|
|
6
6
|
"types": "./dist/index.d.ts",
|
|
@@ -20,8 +20,8 @@
|
|
|
20
20
|
"prepack": "yarn build"
|
|
21
21
|
},
|
|
22
22
|
"dependencies": {
|
|
23
|
-
"logisheets-core": "^1.
|
|
24
|
-
"logisheets-web": "^1.
|
|
23
|
+
"logisheets-core": "^1.13.0",
|
|
24
|
+
"logisheets-web": "^1.13.0"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"esbuild": "^0.21.0",
|