@protobase/server 0.53.0 → 0.53.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.
- package/dist/assistant/batch-write-tool.js +1 -3
- package/dist/assistant/query-tools.js +7 -7
- package/dist/assistant/record-tools.js +5 -11
- package/dist/assistant/write-record-tools.js +2 -4
- package/dist/package.js +1 -1
- package/package.json +4 -4
- package/dist/assistant/budgeted-page.js +0 -30
- package/dist/assistant/query-result-budget.js +0 -32
- package/dist/assistant/record-field-piece.js +0 -54
- package/dist/assistant/result-budget.js +0 -72
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import { HttpProblem } from "../problem.js";
|
|
2
|
-
import { forcedJson } from "./result-budget.js";
|
|
3
2
|
import { defineTool } from "./tools.js";
|
|
4
|
-
import { cutRecordNote } from "./budgeted-page.js";
|
|
5
3
|
import { writeText } from "./write-messages.js";
|
|
6
4
|
import { describedResource } from "./write-prompts.js";
|
|
7
5
|
import { fieldsParameter } from "./write-echo.js";
|
|
@@ -127,7 +125,7 @@ const batchWriteTool = (input = {}) => defineTool({
|
|
|
127
125
|
const answer = await context.approve(approval);
|
|
128
126
|
if (answer !== "approved") return `The user did not approve this batch (${answer}); nothing was written.`;
|
|
129
127
|
const befores = await recordsBefore(context, operations);
|
|
130
|
-
return context.records.batch(operations).then(async (results) =>
|
|
128
|
+
return context.records.batch(operations).then(async (results) => JSON.stringify({ results: await batchEcho(context, operations, results, befores, fields) }), refused);
|
|
131
129
|
}
|
|
132
130
|
});
|
|
133
131
|
//#endregion
|
|
@@ -1,7 +1,7 @@
|
|
|
1
|
-
import { budgetedQueryResult } from "./query-result-budget.js";
|
|
2
1
|
import { runReadOnlyQuery, runReadWriteQuery } from "./run-query.js";
|
|
3
2
|
import { defineTool } from "./tools.js";
|
|
4
3
|
//#region src/assistant/query-tools.ts
|
|
4
|
+
const maxResultLength = 2e4;
|
|
5
5
|
const sqlParameter = (description) => ({
|
|
6
6
|
type: "string",
|
|
7
7
|
description
|
|
@@ -11,7 +11,7 @@ const refused = (error) => {
|
|
|
11
11
|
if (isDatabaseError(error)) return `Postgres refused the statement: ${error.message}`;
|
|
12
12
|
throw error;
|
|
13
13
|
};
|
|
14
|
-
const shown = (result, statement, context
|
|
14
|
+
const shown = (result, statement, context) => {
|
|
15
15
|
context.show({
|
|
16
16
|
type: "table",
|
|
17
17
|
id: crypto.randomUUID(),
|
|
@@ -20,9 +20,9 @@ const shown = (result, statement, context, more) => {
|
|
|
20
20
|
caption: statement,
|
|
21
21
|
...result.truncated && { truncated: true }
|
|
22
22
|
});
|
|
23
|
-
|
|
23
|
+
const json = JSON.stringify(result);
|
|
24
|
+
return json.length > maxResultLength ? `${json.slice(0, maxResultLength)}... (cut off)` : json;
|
|
24
25
|
};
|
|
25
|
-
const readOn = (count) => `; add OFFSET ${count} to the statement to read on`;
|
|
26
26
|
const statementOf = (args) => typeof args.sql === "string" && args.sql.trim() ? args.sql : void 0;
|
|
27
27
|
/**
|
|
28
28
|
* Lets the model read the app's database: one SELECT per call, in a read-only transaction (see `runReadOnlyQuery`).
|
|
@@ -31,7 +31,7 @@ const statementOf = (args) => typeof args.sql === "string" && args.sql.trim() ?
|
|
|
31
31
|
*/
|
|
32
32
|
const readOnlyQueryTool = (db, options = {}) => defineTool({
|
|
33
33
|
name: "run_read_only_query",
|
|
34
|
-
description: `Runs one PostgreSQL SELECT on the app's database in a read-only transaction and returns up to ${options.rowLimit ?? 200} rows as JSON ({ columns, rows, truncated })
|
|
34
|
+
description: `Runs one PostgreSQL SELECT on the app's database in a read-only transaction and returns up to ${options.rowLimit ?? 200} rows as JSON ({ columns, rows, truncated }). The user sees the rows as a table. Use it to answer questions about the data. It reads the app's resource tables only. It cannot change anything.`,
|
|
35
35
|
parameters: {
|
|
36
36
|
type: "object",
|
|
37
37
|
properties: { sql: sqlParameter("One SELECT statement, without a trailing semicolon.") },
|
|
@@ -44,7 +44,7 @@ const readOnlyQueryTool = (db, options = {}) => defineTool({
|
|
|
44
44
|
run: async (args, context) => {
|
|
45
45
|
const statement = statementOf(args);
|
|
46
46
|
if (!statement) return "Pass the statement as { \"sql\": \"select ...\" }.";
|
|
47
|
-
return runReadOnlyQuery(db, statement, options).then((result) => shown(result, statement, context
|
|
47
|
+
return runReadOnlyQuery(db, statement, options).then((result) => shown(result, statement, context), refused);
|
|
48
48
|
}
|
|
49
49
|
});
|
|
50
50
|
/**
|
|
@@ -79,7 +79,7 @@ const readWriteQueryTool = (db, options = {}) => defineTool({
|
|
|
79
79
|
code: statement
|
|
80
80
|
});
|
|
81
81
|
if (answer !== "approved") return `The user did not approve the statement (${answer}); nothing ran.`;
|
|
82
|
-
return runReadWriteQuery(db, statement, options).then((result) => shown(result, statement, context
|
|
82
|
+
return runReadWriteQuery(db, statement, options).then((result) => shown(result, statement, context), refused);
|
|
83
83
|
}
|
|
84
84
|
});
|
|
85
85
|
//#endregion
|
|
@@ -1,9 +1,6 @@
|
|
|
1
1
|
import { notFound } from "../problem.js";
|
|
2
|
-
import { forcedJson } from "./result-budget.js";
|
|
3
2
|
import { defineTool } from "./tools.js";
|
|
4
3
|
import { invalidShowDeleted, keyOf, keyParameter, refused, resourceOf, resourceParameter, showDeletedOf, showDeletedParameter } from "./record-tool-args.js";
|
|
5
|
-
import { budgetedPage, cutRecordNote } from "./budgeted-page.js";
|
|
6
|
-
import { fieldPiece, pieceParameters, readsPiece } from "./record-field-piece.js";
|
|
7
4
|
import { countRecordsTool } from "./count-records-tool.js";
|
|
8
5
|
//#region src/assistant/record-tools.ts
|
|
9
6
|
const readOnly = {
|
|
@@ -78,7 +75,7 @@ const describeResourceTool = () => defineTool({
|
|
|
78
75
|
/** Finds records of a resource with the REST API's filters, sorting and pages, under the caller's access rules. */
|
|
79
76
|
const listRecordsTool = () => defineTool({
|
|
80
77
|
name: "list_records",
|
|
81
|
-
description: `Finds records of a resource and returns a page of them as JSON ({ items, next_page_token, total_size_estimate }). filter uses AIP-160 syntax, such as status = "open" AND amount > 10, and order_by a comma-separated list of fields, each optionally followed by desc. Pass next_page_token back as page_token for the next page; it is empty on the last.
|
|
78
|
+
description: `Finds records of a resource and returns a page of them as JSON ({ items, next_page_token, total_size_estimate }). filter uses AIP-160 syntax, such as status = "open" AND amount > 10, and order_by a comma-separated list of fields, each optionally followed by desc. Pass next_page_token back as page_token for the next page; it is empty on the last. show_deleted: true also returns soft-deleted records.`,
|
|
82
79
|
parameters: {
|
|
83
80
|
type: "object",
|
|
84
81
|
properties: {
|
|
@@ -118,21 +115,19 @@ const listRecordsTool = () => defineTool({
|
|
|
118
115
|
page_size: typeof args.page_size === "number" ? Math.min(args.page_size, maxPageSize) : defaultPageSize,
|
|
119
116
|
...showDeleted && { show_deleted: true }
|
|
120
117
|
};
|
|
121
|
-
|
|
122
|
-
return list(params).then((page) => budgetedPage(page, params, list)).catch(refused);
|
|
118
|
+
return context.records.list(resource, params).then((page) => JSON.stringify(page), refused);
|
|
123
119
|
}
|
|
124
120
|
});
|
|
125
121
|
/** Reads one record by its key, under the caller's access rules. */
|
|
126
122
|
const getRecordTool = () => defineTool({
|
|
127
123
|
name: "get_record",
|
|
128
|
-
description: "Reads one record of a resource by its key and returns it as JSON ({ record, etag })
|
|
124
|
+
description: "Reads one record of a resource by its key and returns it as JSON ({ record, etag }). show_deleted: true also finds a soft-deleted record.",
|
|
129
125
|
parameters: {
|
|
130
126
|
type: "object",
|
|
131
127
|
properties: {
|
|
132
128
|
resource: resourceParameter,
|
|
133
129
|
key: keyParameter,
|
|
134
|
-
show_deleted: showDeletedParameter
|
|
135
|
-
...pieceParameters
|
|
130
|
+
show_deleted: showDeletedParameter
|
|
136
131
|
},
|
|
137
132
|
required: ["resource", "key"]
|
|
138
133
|
},
|
|
@@ -143,8 +138,7 @@ const getRecordTool = () => defineTool({
|
|
|
143
138
|
if (!resource || key === void 0) return "Pass the resource and key as { \"resource\": \"<name>\", \"key\": \"<key>\" }.";
|
|
144
139
|
const showDeleted = showDeletedOf(args);
|
|
145
140
|
if (showDeleted === void 0) return invalidShowDeleted;
|
|
146
|
-
|
|
147
|
-
return context.records.get(resource, key, { show_deleted: showDeleted }).then(shown, refused);
|
|
141
|
+
return context.records.get(resource, key, { show_deleted: showDeleted }).then((record) => JSON.stringify(record), refused);
|
|
148
142
|
}
|
|
149
143
|
});
|
|
150
144
|
/**
|
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import { forcedJson } from "./result-budget.js";
|
|
2
1
|
import { defineTool } from "./tools.js";
|
|
3
2
|
import { keyOf, keyParameter, refused, resourceOf } from "./record-tool-args.js";
|
|
4
|
-
import { cutRecordNote } from "./budgeted-page.js";
|
|
5
3
|
import { createApproval, updateApproval } from "./write-prompts.js";
|
|
6
4
|
import { approvalTexts, recordBefore } from "./record-before.js";
|
|
7
5
|
import { forbiddenText, notApproved, onlyNote, outside, resourceParameterFor } from "./write-tool-limits.js";
|
|
@@ -43,7 +41,7 @@ const createRecordTool = (input = {}) => defineTool({
|
|
|
43
41
|
const { text, described } = await approvalTexts(context, resource);
|
|
44
42
|
const answer = await context.approve(createApproval(text, described, data));
|
|
45
43
|
if (answer !== "approved") return notApproved(answer);
|
|
46
|
-
return context.records.create(resource, data).then((written) =>
|
|
44
|
+
return context.records.create(resource, data).then((written) => JSON.stringify(writeEcho(described.model, written, data, void 0, fields)), refused);
|
|
47
45
|
}
|
|
48
46
|
});
|
|
49
47
|
/** Updates one record by its key once the user approves it, under the caller's access rules. */
|
|
@@ -90,7 +88,7 @@ const updateRecordTool = (input = {}) => defineTool({
|
|
|
90
88
|
const { text, described } = await approvalTexts(context, resource);
|
|
91
89
|
const answer = await context.approve(updateApproval(text, described, key, before?.record ?? {}, data));
|
|
92
90
|
if (answer !== "approved") return notApproved(answer);
|
|
93
|
-
return context.records.update(resource, key, data, { etag }).then((written) =>
|
|
91
|
+
return context.records.update(resource, key, data, { etag }).then((written) => JSON.stringify(writeEcho(described.model, written, data, before?.record, fields)), refused);
|
|
94
92
|
}
|
|
95
93
|
});
|
|
96
94
|
/** `create_record`, `update_record` and `delete_record`, on the resources `input` allows. */
|
package/dist/package.js
CHANGED
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@protobase/server",
|
|
3
|
-
"version": "0.53.
|
|
3
|
+
"version": "0.53.1",
|
|
4
4
|
"description": "The Protobase REST API as a Hono app, with Better Auth sign-in.",
|
|
5
5
|
"license": "Apache-2.0",
|
|
6
6
|
"repository": {
|
|
@@ -40,9 +40,9 @@
|
|
|
40
40
|
"@better-auth/passkey": "^1.7.12",
|
|
41
41
|
"@better-auth/sso": "^1.7.12",
|
|
42
42
|
"@modelcontextprotocol/server": "^2.3.1",
|
|
43
|
-
"@protobase/layout": "0.53.
|
|
44
|
-
"@protobase/query": "0.53.
|
|
45
|
-
"@protobase/schema": "0.53.
|
|
43
|
+
"@protobase/layout": "0.53.1",
|
|
44
|
+
"@protobase/query": "0.53.1",
|
|
45
|
+
"@protobase/schema": "0.53.1",
|
|
46
46
|
"@react-email/render": "^2.1.0",
|
|
47
47
|
"@scalar/hono-api-reference": "^0.12.10",
|
|
48
48
|
"@sentry/core": "^11.7.0",
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import { budgetedJson, forcedJson, leadingFit } from "./result-budget.js";
|
|
2
|
-
//#region src/assistant/budgeted-page.ts
|
|
3
|
-
/** How a record tool's result says to read a field it cut. */
|
|
4
|
-
const cutRecordNote = "The fields in cut_fields were cut to fit the size limit, each with its full length; read the rest of one with get_record and its field, offset and max_chars arguments.";
|
|
5
|
-
const fewerNote = (count) => `Only ${count} records fit the size limit; pass next_page_token as page_token for the next ones. ${cutRecordNote}`;
|
|
6
|
-
/**
|
|
7
|
-
* A page of records as JSON within the size budget: its strings trimmed, then, when that does not fit, the page
|
|
8
|
-
* listed again with as many records as fit, so its next_page_token goes on from the last one returned.
|
|
9
|
-
*/
|
|
10
|
-
const budgetedPage = async (page, params, list) => {
|
|
11
|
-
const json = budgetedJson(page, cutRecordNote);
|
|
12
|
-
if (json) return json;
|
|
13
|
-
if (page.items.length <= 1) return forcedJson(page, cutRecordNote);
|
|
14
|
-
const fits = (count) => budgetedJson({
|
|
15
|
-
...page,
|
|
16
|
-
items: page.items.slice(0, count),
|
|
17
|
-
note: fewerNote(count)
|
|
18
|
-
}, fewerNote(count)) !== void 0;
|
|
19
|
-
const count = leadingFit(page.items.length - 1, fits);
|
|
20
|
-
const fewer = await list({
|
|
21
|
-
...params,
|
|
22
|
-
page_size: count
|
|
23
|
-
});
|
|
24
|
-
return forcedJson({
|
|
25
|
-
...fewer,
|
|
26
|
-
note: fewerNote(fewer.items.length)
|
|
27
|
-
}, fewerNote(fewer.items.length));
|
|
28
|
-
};
|
|
29
|
-
//#endregion
|
|
30
|
-
export { budgetedPage, cutRecordNote };
|
|
@@ -1,32 +0,0 @@
|
|
|
1
|
-
import { budgetedJson, forcedJson, leadingFit } from "./result-budget.js";
|
|
2
|
-
//#region src/assistant/query-result-budget.ts
|
|
3
|
-
const cutCellNote = "The values in cut_fields were cut to fit the size limit, each with its full length (rows[r][c] is row r, column c); read the rest of one with substring(<column> from <start>), counting characters from 1.";
|
|
4
|
-
const fewerRowsNote = (count, more) => `Only the first ${count} rows fit the size limit${more}. ${cutCellNote}`;
|
|
5
|
-
/**
|
|
6
|
-
* A query's result as JSON within the size budget: its strings trimmed, then, when that does not fit, only as many
|
|
7
|
-
* leading rows as fit, marked truncated. `more` ends the note on the rows left out, such as how to read on.
|
|
8
|
-
*/
|
|
9
|
-
const budgetedQueryResult = (result, more) => {
|
|
10
|
-
const json = budgetedJson(result, cutCellNote);
|
|
11
|
-
if (json) return json;
|
|
12
|
-
if (result.rows.length <= 1) return forcedJson(result, cutCellNote);
|
|
13
|
-
const leading = (count) => {
|
|
14
|
-
const note = fewerRowsNote(count, more(count));
|
|
15
|
-
return {
|
|
16
|
-
result: {
|
|
17
|
-
...result,
|
|
18
|
-
rows: result.rows.slice(0, count),
|
|
19
|
-
truncated: true,
|
|
20
|
-
note
|
|
21
|
-
},
|
|
22
|
-
note
|
|
23
|
-
};
|
|
24
|
-
};
|
|
25
|
-
const { result: fewer, note } = leading(leadingFit(result.rows.length - 1, (count) => {
|
|
26
|
-
const { result, note } = leading(count);
|
|
27
|
-
return budgetedJson(result, note) !== void 0;
|
|
28
|
-
}));
|
|
29
|
-
return forcedJson(fewer, note);
|
|
30
|
-
};
|
|
31
|
-
//#endregion
|
|
32
|
-
export { budgetedQueryResult };
|
|
@@ -1,54 +0,0 @@
|
|
|
1
|
-
import { forcedJson } from "./result-budget.js";
|
|
2
|
-
//#region src/assistant/record-field-piece.ts
|
|
3
|
-
/** get_record's arguments that read one field in pieces, such as a field a result cut to fit its size limit. */
|
|
4
|
-
const pieceParameters = {
|
|
5
|
-
field: {
|
|
6
|
-
type: "string",
|
|
7
|
-
description: "One field to read alone, in pieces: max_chars characters of its value from offset. A value that is not text reads as its JSON."
|
|
8
|
-
},
|
|
9
|
-
offset: {
|
|
10
|
-
type: "integer",
|
|
11
|
-
minimum: 0,
|
|
12
|
-
description: "With field: the character to start at, 0 by default."
|
|
13
|
-
},
|
|
14
|
-
max_chars: {
|
|
15
|
-
type: "integer",
|
|
16
|
-
minimum: 1,
|
|
17
|
-
maximum: 3e4,
|
|
18
|
-
description: "With field: the characters to read, 20000 by default."
|
|
19
|
-
}
|
|
20
|
-
};
|
|
21
|
-
const defaultChars = 2e4;
|
|
22
|
-
const maxChars = 3e4;
|
|
23
|
-
const invalid = (message) => JSON.stringify({
|
|
24
|
-
error: "invalid-parameter",
|
|
25
|
-
message
|
|
26
|
-
});
|
|
27
|
-
const countOf = (value, fallback) => value === void 0 ? fallback : typeof value === "number" && Number.isInteger(value) && value >= 0 ? value : void 0;
|
|
28
|
-
/** Whether the call reads one field in pieces rather than the whole record. */
|
|
29
|
-
const readsPiece = (args) => args.field !== void 0;
|
|
30
|
-
/**
|
|
31
|
-
* The piece of `args.field` of `found` the call asks for, as JSON ({ field, offset, length, value, next_offset }), where
|
|
32
|
-
* length is the whole value's and next_offset is absent once the piece reaches its end.
|
|
33
|
-
*/
|
|
34
|
-
const fieldPiece = (found, args) => {
|
|
35
|
-
const field = args.field;
|
|
36
|
-
if (typeof field !== "string" || !Object.hasOwn(found.record, field)) return invalid(`Invalid parameter field: pass one of ${Object.keys(found.record).join(", ")}`);
|
|
37
|
-
const offset = countOf(args.offset, 0);
|
|
38
|
-
if (offset === void 0) return invalid("Invalid parameter offset: pass a whole number from 0");
|
|
39
|
-
const chars = countOf(args.max_chars, defaultChars);
|
|
40
|
-
if (!chars) return invalid(`Invalid parameter max_chars: pass a whole number from 1 to ${maxChars}`);
|
|
41
|
-
const raw = found.record[field];
|
|
42
|
-
const text = typeof raw === "string" ? raw : JSON.stringify(raw) ?? "null";
|
|
43
|
-
const end = offset + Math.min(chars, maxChars);
|
|
44
|
-
const piece = {
|
|
45
|
-
field,
|
|
46
|
-
offset,
|
|
47
|
-
length: text.length,
|
|
48
|
-
value: text.slice(offset, end),
|
|
49
|
-
...end < text.length && { next_offset: end }
|
|
50
|
-
};
|
|
51
|
-
return forcedJson(piece, "The value was cut to fit the size limit; pass a smaller max_chars.");
|
|
52
|
-
};
|
|
53
|
-
//#endregion
|
|
54
|
-
export { fieldPiece, pieceParameters, readsPiece };
|
|
@@ -1,72 +0,0 @@
|
|
|
1
|
-
//#region src/assistant/result-budget.ts
|
|
2
|
-
/**
|
|
3
|
-
* Keeps a tool result within what a model's client takes: over the budget, its longest string values are trimmed, each
|
|
4
|
-
* named in `cut_fields` with its full length, and a `note` says how to read the rest. A tool with a list returns fewer
|
|
5
|
-
* items when trimming alone does not fit (see `leadingFit`).
|
|
6
|
-
*/
|
|
7
|
-
/** The characters of JSON one tool result may hold. */
|
|
8
|
-
const resultBudget = 4e4;
|
|
9
|
-
const stringFloor = 500;
|
|
10
|
-
const neverCut = /* @__PURE__ */ new Set(["next_page_token", "etag"]);
|
|
11
|
-
const pathOf = (path, key) => path ? `${path}.${key}` : key;
|
|
12
|
-
const trimmed = (value, cap, path, cuts) => {
|
|
13
|
-
if (typeof value === "string") {
|
|
14
|
-
if (value.length <= cap) return value;
|
|
15
|
-
cuts.push({
|
|
16
|
-
field: path,
|
|
17
|
-
length: value.length
|
|
18
|
-
});
|
|
19
|
-
return `${value.slice(0, cap)}…`;
|
|
20
|
-
}
|
|
21
|
-
if (Array.isArray(value)) return value.map((item, index) => trimmed(item, cap, `${path}[${index}]`, cuts));
|
|
22
|
-
if (value !== null && typeof value === "object") return Object.fromEntries(Object.entries(value).map(([key, item]) => [key, !path && neverCut.has(key) ? item : trimmed(item, cap, pathOf(path, key), cuts)]));
|
|
23
|
-
return value;
|
|
24
|
-
};
|
|
25
|
-
const longestString = (value) => {
|
|
26
|
-
if (typeof value === "string") return value.length;
|
|
27
|
-
if (value !== null && typeof value === "object") return Math.max(0, ...Object.values(value).map(longestString));
|
|
28
|
-
return 0;
|
|
29
|
-
};
|
|
30
|
-
const cutAt = (result, cap, note) => {
|
|
31
|
-
const cuts = [];
|
|
32
|
-
const value = trimmed(result, cap, "", cuts);
|
|
33
|
-
return JSON.stringify(cuts.length ? {
|
|
34
|
-
...value,
|
|
35
|
-
cut_fields: cuts,
|
|
36
|
-
note
|
|
37
|
-
} : value);
|
|
38
|
-
};
|
|
39
|
-
/**
|
|
40
|
-
* `result` as JSON within `budget`, its strings trimmed as little as fits but to no fewer than `floor` characters;
|
|
41
|
-
* undefined when even that does not fit. `note` says how to read a cut field's rest.
|
|
42
|
-
*/
|
|
43
|
-
const budgetedJson = (result, note, budget = resultBudget, floor = stringFloor) => {
|
|
44
|
-
const json = JSON.stringify(result);
|
|
45
|
-
if (json.length <= budget) return json;
|
|
46
|
-
const plain = JSON.parse(json);
|
|
47
|
-
let low = floor;
|
|
48
|
-
let high = longestString(plain);
|
|
49
|
-
if (low >= high) return void 0;
|
|
50
|
-
if (cutAt(plain, low, note).length > budget) return void 0;
|
|
51
|
-
while (high - low > 1) {
|
|
52
|
-
const cap = Math.floor((low + high) / 2);
|
|
53
|
-
if (cutAt(plain, cap, note).length <= budget) low = cap;
|
|
54
|
-
else high = cap;
|
|
55
|
-
}
|
|
56
|
-
return cutAt(plain, low, note);
|
|
57
|
-
};
|
|
58
|
-
/** `result` as JSON within `budget` if it can be, its strings trimmed down to nothing when they must; never undefined. */
|
|
59
|
-
const forcedJson = (result, note, budget = resultBudget) => budgetedJson(result, note, budget) ?? budgetedJson(result, note, budget, 0) ?? cutAt(JSON.parse(JSON.stringify(result)), 0, note);
|
|
60
|
-
/** The most leading items of `count`, at least 1, for which `fits` holds, `fits` holding for fewer when it does for more. */
|
|
61
|
-
const leadingFit = (count, fits) => {
|
|
62
|
-
let low = 1;
|
|
63
|
-
let high = count;
|
|
64
|
-
while (low < high) {
|
|
65
|
-
const middle = Math.ceil((low + high) / 2);
|
|
66
|
-
if (fits(middle)) low = middle;
|
|
67
|
-
else high = middle - 1;
|
|
68
|
-
}
|
|
69
|
-
return low;
|
|
70
|
-
};
|
|
71
|
-
//#endregion
|
|
72
|
-
export { budgetedJson, forcedJson, leadingFit, resultBudget };
|