@sohqureshi/tokenwise 1.0.9 → 1.0.10

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 CHANGED
@@ -147,9 +147,25 @@ toNatural({
147
147
  }
148
148
  }
149
149
  });
150
- // policy: holder name Carlos Rivera, policy number HLT-2048, claim: status under review, requested amount 64000.
150
+ // Carlos Rivera has policy HLT-2048. The claim is under review for 64,000.
151
151
  ```
152
152
 
153
+ `toNatural()` uses semantic templates when the field relationships are
154
+ unambiguous (for example, a policy holder, policy number, and claim). Unknown
155
+ JSON shapes use a conservative key-value fallback so the formatter does not
156
+ invent relationships or facts. For dynamic entity-shaped objects, it can
157
+ recognize common subject fields such as `name`, `title`, `label`, `displayName`,
158
+ `entityName`, `fullName`, `userName`, `customerName`, `ownerName`,
159
+ `accountName`, `companyName`, `organizationName`, `teamName`, `projectName`,
160
+ `productName`, `serviceName`, `resourceName`, `deviceName`, `patientName`,
161
+ `taskName`, and `eventName`. These can be paired with state fields such as
162
+ `status`, `state`, `condition`, `stage`, `role`, `type`, `category`, `priority`,
163
+ `phase`, `mode`, `availability`, `outcome`, `result`, `health`, `progress`,
164
+ `visibility`, `access`, `membership`, `sentiment`, and `severity`.
165
+ Matching is case-insensitive and also supports keys that contain these
166
+ concepts, such as `patientName`, `orderStatus`, `currentPhase`, or
167
+ `incidentSeverity`.
168
+
153
169
  ### `toTOON()`
154
170
 
155
171
  Converts JSON into a compact TOON-like text format. Arrays of objects become table-style rows.
@@ -220,14 +236,27 @@ used by `tiktoken`.
220
236
 
221
237
  ## Release Notes
222
238
 
239
+ ### v1.0.10 — 2026-09-13
240
+
241
+ - Add conservative contains-based semantic matching for dynamic subject and state keys, including fields such as `patientName`, `orderStatus`, and `incidentSeverity`.
242
+ - Expand `toNatural()` regression coverage for dynamic entities, nested objects, arrays, metadata filtering, and alternate subject fields.
243
+ - Update the browser demo to use the v1.0.10 CDN package.
244
+
223
245
  ### v1.0.8 — 2026-09-06
224
246
 
225
247
  - Expose exact tokenizer metadata when available (model + encoding), and fall back to a clear, model-aware estimator in browser demos.
226
248
  - Demo updated to show selected model and expected encoding when the exact tokenizer (tiktoken) is not available in-browser.
227
249
  - Updated analyze() to surface tokenizer encoding in analysis output so the demo shows "Estimator: model=gpt-4 expected_encoding=cl100k_base" even when using the heuristic fallback.
228
- - Misc: build artifacts updated and docs demo import bumped to v1.0.7 CDN bundle.
250
+ - Misc: build artifacts updated.
251
+
252
+ ### v1.0.9 — 2026-09-13
229
253
 
230
- If you want the browser demo to display truly exact token counts, run the demo against a small Node endpoint (or local server) that has tiktoken installed and uses analyze(..., { exact: true }). The estimator will display the real tokenizer encoding when tiktoken is present.
254
+ - Use `tiktoken` model-aware token counts by default in `estimateTokens()` and `analyze()`.
255
+ - Keep the four-characters-per-token estimate available with `exact: false`.
256
+ - Correctly identify `o200k_base` for modern OpenAI models such as `gpt-4o`, `gpt-4.1`, `o1`, and `o3`.
257
+ - Update the browser demo to use the v1.0.9 CDN package.
258
+ - Make demo analysis actionable by showing signed token and character changes, the selected transformation, and an explanation of whether the result is cheaper or larger.
259
+ - Rename the demo's input-only Analyze view to Inspect, showing input size and structure without applying a transformation.
231
260
 
232
261
  ---
233
262
 
@@ -0,0 +1,241 @@
1
+ // src/core/natural.ts
2
+ function toNatural(data, depth = 0) {
3
+ if (data === null || data === void 0) return "nothing";
4
+ if (typeof data === "string") return data;
5
+ if (typeof data === "number") return String(data);
6
+ if (typeof data === "boolean") return data ? "yes" : "no";
7
+ if (Array.isArray(data)) {
8
+ if (data.length === 0) return "empty list";
9
+ if (data.every(isPlainObject)) {
10
+ return data.map((item, index) => `${index + 1}. ${stripTrailingPeriod(toNatural(item, depth + 1))}.`).join(" ");
11
+ }
12
+ const items = data.map((item) => toNatural(item, depth + 1));
13
+ return joinNaturalList(items);
14
+ }
15
+ if (typeof data === "object") {
16
+ return buildContextualStory(data, depth);
17
+ }
18
+ return String(data);
19
+ }
20
+ function buildContextualStory(obj, depth = 0) {
21
+ const semanticStory = buildSemanticStory(obj);
22
+ if (semanticStory) return semanticStory;
23
+ const nestedEntity = Object.values(obj).find((value) => {
24
+ return isPlainObject(value) && buildSemanticStory(value) !== null;
25
+ });
26
+ if (nestedEntity) {
27
+ const nestedStory = buildSemanticStory(nestedEntity);
28
+ if (nestedStory) return nestedStory;
29
+ }
30
+ let name = obj.name || obj.userName || obj.user;
31
+ if (typeof name === "object" && name !== null && name.name) {
32
+ name = name.name;
33
+ }
34
+ const entries = Object.entries(obj).filter(([key]) => {
35
+ return !["id", "timestamp", "apiKey"].includes(key);
36
+ });
37
+ if (entries.length === 0) return "";
38
+ let story = name && typeof name === "string" ? `User ${name}` : "";
39
+ const clauses = entries.map(([key, value]) => {
40
+ if (key === "user" && name && typeof value === "object" && value !== null && !Array.isArray(value)) {
41
+ const userProps = Object.entries(value).filter(([k]) => !["id", "name", "timestamp", "apiKey"].includes(k)).map(([k, v]) => formatPropertyClause(k, v, depth + 1)).filter((c) => c !== null && c !== "").join(", ");
42
+ return userProps ? `(${userProps})` : null;
43
+ }
44
+ return formatPropertyClause(key, value, depth);
45
+ }).filter((c) => c !== null && c !== "");
46
+ if (clauses.length === 0) return story;
47
+ if (story) {
48
+ story += " " + clauses.join(", ");
49
+ } else {
50
+ story = clauses.join(", ");
51
+ }
52
+ return story + ".";
53
+ }
54
+ function buildSemanticStory(obj) {
55
+ if (typeof obj.holderName === "string" && typeof obj.policyNumber === "string") {
56
+ const subject2 = `${obj.holderName} has policy ${obj.policyNumber}`;
57
+ const details = [
58
+ typeof obj.planType === "string" ? `a ${obj.planType} plan` : null,
59
+ typeof obj.premiumAmount === "number" ? `with a premium of ${formatNumber(obj.premiumAmount)}` : null
60
+ ].filter((value) => value !== null);
61
+ const sentences = [`${subject2}${details.length ? `, ${details.join(", ")}` : ""}.`];
62
+ if (isPlainObject(obj.claim)) {
63
+ const claim = describeClaim(obj.claim);
64
+ if (claim) sentences.push(claim);
65
+ }
66
+ if (Array.isArray(obj.dependents) && obj.dependents.length > 0) {
67
+ sentences.push(`${obj.holderName}'s dependents are ${joinNaturalList(obj.dependents.map(String))}.`);
68
+ }
69
+ return sentences.join(" ");
70
+ }
71
+ const subject = findSubject(obj);
72
+ const stateEntry = findSemanticEntry(obj, [
73
+ "status",
74
+ "state",
75
+ "condition",
76
+ "stage",
77
+ "role",
78
+ "type",
79
+ "category",
80
+ "classification",
81
+ "priority",
82
+ "phase",
83
+ "mode",
84
+ "availability",
85
+ "outcome",
86
+ "result",
87
+ "health",
88
+ "progress",
89
+ "visibility",
90
+ "access",
91
+ "membership",
92
+ "sentiment",
93
+ "severity"
94
+ ]);
95
+ if (subject && stateEntry && typeof stateEntry[1] === "string") {
96
+ const details = Object.entries(obj).filter(([key, value]) => key !== stateEntry[0] && key !== subject.key && shouldDescribeSemantically(key, value)).map(([key, value]) => `${camelToWords(key)} ${formatSemanticValue(value)}`);
97
+ return `${subject.value} is ${stateEntry[1]}${details.length ? ` and has ${details.join(", ")}` : ""}.`;
98
+ }
99
+ return null;
100
+ }
101
+ function findSubject(obj) {
102
+ const subjectKeys = [
103
+ "name",
104
+ "title",
105
+ "label",
106
+ "displayName",
107
+ "entityName",
108
+ "fullName",
109
+ "userName",
110
+ "username",
111
+ "personName",
112
+ "customerName",
113
+ "clientName",
114
+ "ownerName",
115
+ "accountName",
116
+ "companyName",
117
+ "organizationName",
118
+ "teamName",
119
+ "departmentName",
120
+ "projectName",
121
+ "productName",
122
+ "serviceName",
123
+ "resourceName",
124
+ "fileName",
125
+ "deviceName",
126
+ "hostName",
127
+ "applicationName",
128
+ "appName",
129
+ "taskName",
130
+ "eventName",
131
+ "itemName",
132
+ "orderName",
133
+ "patientName",
134
+ "employeeName"
135
+ ];
136
+ const entries = Object.entries(obj);
137
+ for (const key of subjectKeys) {
138
+ if (typeof obj[key] === "string" && obj[key].trim()) {
139
+ return { key, value: obj[key].trim() };
140
+ }
141
+ }
142
+ const nameEntry = entries.find(([key, value]) => {
143
+ return key.toLowerCase().includes("name") && typeof value === "string" && value.trim().length > 0;
144
+ });
145
+ if (nameEntry) {
146
+ return { key: nameEntry[0], value: nameEntry[1].trim() };
147
+ }
148
+ return null;
149
+ }
150
+ function findSemanticEntry(obj, keys) {
151
+ const entries = Object.entries(obj);
152
+ const exactEntry = entries.find(([key]) => keys.includes(key));
153
+ if (exactEntry) return exactEntry;
154
+ return entries.find(([key]) => {
155
+ const normalizedKey = key.toLowerCase();
156
+ return keys.some((semanticKey) => normalizedKey.includes(semanticKey.toLowerCase()));
157
+ }) ?? null;
158
+ }
159
+ function shouldDescribeSemantically(key, value) {
160
+ return (typeof value === "string" || typeof value === "number" || typeof value === "boolean") && !["id", "timestamp", "createdAt", "updatedAt", "apiKey", "debug"].includes(key);
161
+ }
162
+ function formatSemanticValue(value) {
163
+ if (typeof value === "number") return formatNumber(value);
164
+ if (typeof value === "boolean") return value ? "enabled" : "disabled";
165
+ return String(value);
166
+ }
167
+ function describeClaim(claim) {
168
+ const parts = [];
169
+ if (typeof claim.status === "string") parts.push(`The claim is ${claim.status}`);
170
+ if (typeof claim.requestedAmount === "number") {
171
+ parts.push(`for ${formatNumber(claim.requestedAmount)}`);
172
+ }
173
+ if (typeof claim.claimNumber === "string") {
174
+ parts.push(`(reference ${claim.claimNumber})`);
175
+ }
176
+ return parts.length ? `${parts.join(" ")}.` : null;
177
+ }
178
+ function formatNumber(value) {
179
+ return new Intl.NumberFormat("en-US").format(value);
180
+ }
181
+ function formatPropertyClause(key, value, depth) {
182
+ const naturalKey = camelToWords(key);
183
+ if (typeof value === "object" && value !== null && !Array.isArray(value)) {
184
+ if (["internal", "metadata", "debug"].includes(key)) {
185
+ return null;
186
+ }
187
+ const nested = Object.entries(value).filter(([k]) => !["id", "timestamp", "apiKey", "createdAt", "updatedAt"].includes(k)).map(([k, v]) => {
188
+ if (typeof v === "object" && v !== null) {
189
+ return formatPropertyClause(k, v, depth + 1);
190
+ }
191
+ const propValue = toNatural(v, depth + 1);
192
+ if (propValue === "yes" || propValue === "no") {
193
+ return `${camelToWords(k)} ${propValue === "yes" ? "enabled" : "disabled"}`;
194
+ }
195
+ return `${camelToWords(k)} ${propValue}`;
196
+ }).filter((c) => c && c.trim()).join(", ");
197
+ if (!nested) return null;
198
+ return `${naturalKey}: ${nested}`;
199
+ }
200
+ if (Array.isArray(value)) {
201
+ if (value.length === 0) return null;
202
+ if (["skills", "hobbies", "interests", "tags", "languages", "items"].includes(key)) {
203
+ const items2 = value.map((item) => {
204
+ const str = String(item);
205
+ return str.charAt(0).toUpperCase() + str.slice(1);
206
+ });
207
+ return `Having ${joinNaturalList(items2)}`;
208
+ }
209
+ const items = joinNaturalList(value.map((item) => toNatural(item, depth + 1)));
210
+ return `${naturalKey}: ${items}`;
211
+ }
212
+ const naturalValue = toNatural(value, depth + 1);
213
+ if (naturalValue === "yes") return `${naturalKey} enabled`;
214
+ if (naturalValue === "no") return `${naturalKey} disabled`;
215
+ if (key === "theme") return `prefers the ${naturalValue} ${key}`;
216
+ if (key === "age") return `age: ${naturalValue}`;
217
+ if (key === "email") return `email: ${naturalValue}`;
218
+ if (key === "city") return `address: ${naturalValue}`;
219
+ if (key === "debug" && naturalValue === "yes") return `debug enabled`;
220
+ return `${naturalKey}: ${naturalValue}`;
221
+ }
222
+ function camelToWords(str) {
223
+ return str.replace(/([A-Z])/g, " $1").toLowerCase().trim();
224
+ }
225
+ function isPlainObject(value) {
226
+ return typeof value === "object" && value !== null && !Array.isArray(value);
227
+ }
228
+ function joinNaturalList(items) {
229
+ if (items.length === 0) return "";
230
+ if (items.length === 1) return items[0];
231
+ if (items.length === 2) return `${items[0]} and ${items[1]}`;
232
+ const lastItem = items[items.length - 1];
233
+ return `${items.slice(0, -1).join(", ")} and ${lastItem}`;
234
+ }
235
+ function stripTrailingPeriod(value) {
236
+ return value.replace(/\.+$/, "");
237
+ }
238
+
239
+ export {
240
+ toNatural
241
+ };
@@ -12,7 +12,7 @@ import {
12
12
  } from "./chunk-L7BC62MT.js";
13
13
  import {
14
14
  toNatural
15
- } from "./chunk-D4CFTFM3.js";
15
+ } from "./chunk-BN3L7TUV.js";
16
16
  import {
17
17
  prune
18
18
  } from "./chunk-ZD536GZF.js";
package/dist/cli.cjs CHANGED
@@ -285,6 +285,15 @@ function toNatural(data, depth = 0) {
285
285
  return String(data);
286
286
  }
287
287
  function buildContextualStory(obj, depth = 0) {
288
+ const semanticStory = buildSemanticStory(obj);
289
+ if (semanticStory) return semanticStory;
290
+ const nestedEntity = Object.values(obj).find((value) => {
291
+ return isPlainObject2(value) && buildSemanticStory(value) !== null;
292
+ });
293
+ if (nestedEntity) {
294
+ const nestedStory = buildSemanticStory(nestedEntity);
295
+ if (nestedStory) return nestedStory;
296
+ }
288
297
  let name = obj.name || obj.userName || obj.user;
289
298
  if (typeof name === "object" && name !== null && name.name) {
290
299
  name = name.name;
@@ -309,6 +318,133 @@ function buildContextualStory(obj, depth = 0) {
309
318
  }
310
319
  return story + ".";
311
320
  }
321
+ function buildSemanticStory(obj) {
322
+ if (typeof obj.holderName === "string" && typeof obj.policyNumber === "string") {
323
+ const subject2 = `${obj.holderName} has policy ${obj.policyNumber}`;
324
+ const details = [
325
+ typeof obj.planType === "string" ? `a ${obj.planType} plan` : null,
326
+ typeof obj.premiumAmount === "number" ? `with a premium of ${formatNumber(obj.premiumAmount)}` : null
327
+ ].filter((value) => value !== null);
328
+ const sentences = [`${subject2}${details.length ? `, ${details.join(", ")}` : ""}.`];
329
+ if (isPlainObject2(obj.claim)) {
330
+ const claim = describeClaim(obj.claim);
331
+ if (claim) sentences.push(claim);
332
+ }
333
+ if (Array.isArray(obj.dependents) && obj.dependents.length > 0) {
334
+ sentences.push(`${obj.holderName}'s dependents are ${joinNaturalList(obj.dependents.map(String))}.`);
335
+ }
336
+ return sentences.join(" ");
337
+ }
338
+ const subject = findSubject(obj);
339
+ const stateEntry = findSemanticEntry(obj, [
340
+ "status",
341
+ "state",
342
+ "condition",
343
+ "stage",
344
+ "role",
345
+ "type",
346
+ "category",
347
+ "classification",
348
+ "priority",
349
+ "phase",
350
+ "mode",
351
+ "availability",
352
+ "outcome",
353
+ "result",
354
+ "health",
355
+ "progress",
356
+ "visibility",
357
+ "access",
358
+ "membership",
359
+ "sentiment",
360
+ "severity"
361
+ ]);
362
+ if (subject && stateEntry && typeof stateEntry[1] === "string") {
363
+ const details = Object.entries(obj).filter(([key, value]) => key !== stateEntry[0] && key !== subject.key && shouldDescribeSemantically(key, value)).map(([key, value]) => `${camelToWords(key)} ${formatSemanticValue(value)}`);
364
+ return `${subject.value} is ${stateEntry[1]}${details.length ? ` and has ${details.join(", ")}` : ""}.`;
365
+ }
366
+ return null;
367
+ }
368
+ function findSubject(obj) {
369
+ const subjectKeys = [
370
+ "name",
371
+ "title",
372
+ "label",
373
+ "displayName",
374
+ "entityName",
375
+ "fullName",
376
+ "userName",
377
+ "username",
378
+ "personName",
379
+ "customerName",
380
+ "clientName",
381
+ "ownerName",
382
+ "accountName",
383
+ "companyName",
384
+ "organizationName",
385
+ "teamName",
386
+ "departmentName",
387
+ "projectName",
388
+ "productName",
389
+ "serviceName",
390
+ "resourceName",
391
+ "fileName",
392
+ "deviceName",
393
+ "hostName",
394
+ "applicationName",
395
+ "appName",
396
+ "taskName",
397
+ "eventName",
398
+ "itemName",
399
+ "orderName",
400
+ "patientName",
401
+ "employeeName"
402
+ ];
403
+ const entries = Object.entries(obj);
404
+ for (const key of subjectKeys) {
405
+ if (typeof obj[key] === "string" && obj[key].trim()) {
406
+ return { key, value: obj[key].trim() };
407
+ }
408
+ }
409
+ const nameEntry = entries.find(([key, value]) => {
410
+ return key.toLowerCase().includes("name") && typeof value === "string" && value.trim().length > 0;
411
+ });
412
+ if (nameEntry) {
413
+ return { key: nameEntry[0], value: nameEntry[1].trim() };
414
+ }
415
+ return null;
416
+ }
417
+ function findSemanticEntry(obj, keys) {
418
+ const entries = Object.entries(obj);
419
+ const exactEntry = entries.find(([key]) => keys.includes(key));
420
+ if (exactEntry) return exactEntry;
421
+ return entries.find(([key]) => {
422
+ const normalizedKey = key.toLowerCase();
423
+ return keys.some((semanticKey) => normalizedKey.includes(semanticKey.toLowerCase()));
424
+ }) ?? null;
425
+ }
426
+ function shouldDescribeSemantically(key, value) {
427
+ return (typeof value === "string" || typeof value === "number" || typeof value === "boolean") && !["id", "timestamp", "createdAt", "updatedAt", "apiKey", "debug"].includes(key);
428
+ }
429
+ function formatSemanticValue(value) {
430
+ if (typeof value === "number") return formatNumber(value);
431
+ if (typeof value === "boolean") return value ? "enabled" : "disabled";
432
+ return String(value);
433
+ }
434
+ function describeClaim(claim) {
435
+ const parts = [];
436
+ if (typeof claim.status === "string") parts.push(`The claim is ${claim.status}`);
437
+ if (typeof claim.requestedAmount === "number") {
438
+ parts.push(`for ${formatNumber(claim.requestedAmount)}`);
439
+ }
440
+ if (typeof claim.claimNumber === "string") {
441
+ parts.push(`(reference ${claim.claimNumber})`);
442
+ }
443
+ return parts.length ? `${parts.join(" ")}.` : null;
444
+ }
445
+ function formatNumber(value) {
446
+ return new Intl.NumberFormat("en-US").format(value);
447
+ }
312
448
  function formatPropertyClause(key, value, depth) {
313
449
  const naturalKey = camelToWords(key);
314
450
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
package/dist/cli.js CHANGED
@@ -1,12 +1,12 @@
1
1
  #!/usr/bin/env node
2
2
  import {
3
3
  AIChain
4
- } from "./chunk-QHCUNOUH.js";
4
+ } from "./chunk-FBJ3DHS6.js";
5
5
  import "./chunk-2TDVL6BJ.js";
6
6
  import "./chunk-6VRLDRJK.js";
7
7
  import "./chunk-XE36GLJP.js";
8
8
  import "./chunk-L7BC62MT.js";
9
- import "./chunk-D4CFTFM3.js";
9
+ import "./chunk-BN3L7TUV.js";
10
10
  import "./chunk-ZD536GZF.js";
11
11
  import "./chunk-L3HY6AWL.js";
12
12
 
@@ -42,6 +42,15 @@ function toNatural(data, depth = 0) {
42
42
  return String(data);
43
43
  }
44
44
  function buildContextualStory(obj, depth = 0) {
45
+ const semanticStory = buildSemanticStory(obj);
46
+ if (semanticStory) return semanticStory;
47
+ const nestedEntity = Object.values(obj).find((value) => {
48
+ return isPlainObject(value) && buildSemanticStory(value) !== null;
49
+ });
50
+ if (nestedEntity) {
51
+ const nestedStory = buildSemanticStory(nestedEntity);
52
+ if (nestedStory) return nestedStory;
53
+ }
45
54
  let name = obj.name || obj.userName || obj.user;
46
55
  if (typeof name === "object" && name !== null && name.name) {
47
56
  name = name.name;
@@ -66,6 +75,133 @@ function buildContextualStory(obj, depth = 0) {
66
75
  }
67
76
  return story + ".";
68
77
  }
78
+ function buildSemanticStory(obj) {
79
+ if (typeof obj.holderName === "string" && typeof obj.policyNumber === "string") {
80
+ const subject2 = `${obj.holderName} has policy ${obj.policyNumber}`;
81
+ const details = [
82
+ typeof obj.planType === "string" ? `a ${obj.planType} plan` : null,
83
+ typeof obj.premiumAmount === "number" ? `with a premium of ${formatNumber(obj.premiumAmount)}` : null
84
+ ].filter((value) => value !== null);
85
+ const sentences = [`${subject2}${details.length ? `, ${details.join(", ")}` : ""}.`];
86
+ if (isPlainObject(obj.claim)) {
87
+ const claim = describeClaim(obj.claim);
88
+ if (claim) sentences.push(claim);
89
+ }
90
+ if (Array.isArray(obj.dependents) && obj.dependents.length > 0) {
91
+ sentences.push(`${obj.holderName}'s dependents are ${joinNaturalList(obj.dependents.map(String))}.`);
92
+ }
93
+ return sentences.join(" ");
94
+ }
95
+ const subject = findSubject(obj);
96
+ const stateEntry = findSemanticEntry(obj, [
97
+ "status",
98
+ "state",
99
+ "condition",
100
+ "stage",
101
+ "role",
102
+ "type",
103
+ "category",
104
+ "classification",
105
+ "priority",
106
+ "phase",
107
+ "mode",
108
+ "availability",
109
+ "outcome",
110
+ "result",
111
+ "health",
112
+ "progress",
113
+ "visibility",
114
+ "access",
115
+ "membership",
116
+ "sentiment",
117
+ "severity"
118
+ ]);
119
+ if (subject && stateEntry && typeof stateEntry[1] === "string") {
120
+ const details = Object.entries(obj).filter(([key, value]) => key !== stateEntry[0] && key !== subject.key && shouldDescribeSemantically(key, value)).map(([key, value]) => `${camelToWords(key)} ${formatSemanticValue(value)}`);
121
+ return `${subject.value} is ${stateEntry[1]}${details.length ? ` and has ${details.join(", ")}` : ""}.`;
122
+ }
123
+ return null;
124
+ }
125
+ function findSubject(obj) {
126
+ const subjectKeys = [
127
+ "name",
128
+ "title",
129
+ "label",
130
+ "displayName",
131
+ "entityName",
132
+ "fullName",
133
+ "userName",
134
+ "username",
135
+ "personName",
136
+ "customerName",
137
+ "clientName",
138
+ "ownerName",
139
+ "accountName",
140
+ "companyName",
141
+ "organizationName",
142
+ "teamName",
143
+ "departmentName",
144
+ "projectName",
145
+ "productName",
146
+ "serviceName",
147
+ "resourceName",
148
+ "fileName",
149
+ "deviceName",
150
+ "hostName",
151
+ "applicationName",
152
+ "appName",
153
+ "taskName",
154
+ "eventName",
155
+ "itemName",
156
+ "orderName",
157
+ "patientName",
158
+ "employeeName"
159
+ ];
160
+ const entries = Object.entries(obj);
161
+ for (const key of subjectKeys) {
162
+ if (typeof obj[key] === "string" && obj[key].trim()) {
163
+ return { key, value: obj[key].trim() };
164
+ }
165
+ }
166
+ const nameEntry = entries.find(([key, value]) => {
167
+ return key.toLowerCase().includes("name") && typeof value === "string" && value.trim().length > 0;
168
+ });
169
+ if (nameEntry) {
170
+ return { key: nameEntry[0], value: nameEntry[1].trim() };
171
+ }
172
+ return null;
173
+ }
174
+ function findSemanticEntry(obj, keys) {
175
+ const entries = Object.entries(obj);
176
+ const exactEntry = entries.find(([key]) => keys.includes(key));
177
+ if (exactEntry) return exactEntry;
178
+ return entries.find(([key]) => {
179
+ const normalizedKey = key.toLowerCase();
180
+ return keys.some((semanticKey) => normalizedKey.includes(semanticKey.toLowerCase()));
181
+ }) ?? null;
182
+ }
183
+ function shouldDescribeSemantically(key, value) {
184
+ return (typeof value === "string" || typeof value === "number" || typeof value === "boolean") && !["id", "timestamp", "createdAt", "updatedAt", "apiKey", "debug"].includes(key);
185
+ }
186
+ function formatSemanticValue(value) {
187
+ if (typeof value === "number") return formatNumber(value);
188
+ if (typeof value === "boolean") return value ? "enabled" : "disabled";
189
+ return String(value);
190
+ }
191
+ function describeClaim(claim) {
192
+ const parts = [];
193
+ if (typeof claim.status === "string") parts.push(`The claim is ${claim.status}`);
194
+ if (typeof claim.requestedAmount === "number") {
195
+ parts.push(`for ${formatNumber(claim.requestedAmount)}`);
196
+ }
197
+ if (typeof claim.claimNumber === "string") {
198
+ parts.push(`(reference ${claim.claimNumber})`);
199
+ }
200
+ return parts.length ? `${parts.join(" ")}.` : null;
201
+ }
202
+ function formatNumber(value) {
203
+ return new Intl.NumberFormat("en-US").format(value);
204
+ }
69
205
  function formatPropertyClause(key, value, depth) {
70
206
  const naturalKey = camelToWords(key);
71
207
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  toNatural
3
- } from "../chunk-D4CFTFM3.js";
3
+ } from "../chunk-BN3L7TUV.js";
4
4
  export {
5
5
  toNatural
6
6
  };
package/dist/index.cjs CHANGED
@@ -294,6 +294,15 @@ function toNatural(data, depth = 0) {
294
294
  return String(data);
295
295
  }
296
296
  function buildContextualStory(obj, depth = 0) {
297
+ const semanticStory = buildSemanticStory(obj);
298
+ if (semanticStory) return semanticStory;
299
+ const nestedEntity = Object.values(obj).find((value) => {
300
+ return isPlainObject2(value) && buildSemanticStory(value) !== null;
301
+ });
302
+ if (nestedEntity) {
303
+ const nestedStory = buildSemanticStory(nestedEntity);
304
+ if (nestedStory) return nestedStory;
305
+ }
297
306
  let name = obj.name || obj.userName || obj.user;
298
307
  if (typeof name === "object" && name !== null && name.name) {
299
308
  name = name.name;
@@ -318,6 +327,133 @@ function buildContextualStory(obj, depth = 0) {
318
327
  }
319
328
  return story + ".";
320
329
  }
330
+ function buildSemanticStory(obj) {
331
+ if (typeof obj.holderName === "string" && typeof obj.policyNumber === "string") {
332
+ const subject2 = `${obj.holderName} has policy ${obj.policyNumber}`;
333
+ const details = [
334
+ typeof obj.planType === "string" ? `a ${obj.planType} plan` : null,
335
+ typeof obj.premiumAmount === "number" ? `with a premium of ${formatNumber(obj.premiumAmount)}` : null
336
+ ].filter((value) => value !== null);
337
+ const sentences = [`${subject2}${details.length ? `, ${details.join(", ")}` : ""}.`];
338
+ if (isPlainObject2(obj.claim)) {
339
+ const claim = describeClaim(obj.claim);
340
+ if (claim) sentences.push(claim);
341
+ }
342
+ if (Array.isArray(obj.dependents) && obj.dependents.length > 0) {
343
+ sentences.push(`${obj.holderName}'s dependents are ${joinNaturalList(obj.dependents.map(String))}.`);
344
+ }
345
+ return sentences.join(" ");
346
+ }
347
+ const subject = findSubject(obj);
348
+ const stateEntry = findSemanticEntry(obj, [
349
+ "status",
350
+ "state",
351
+ "condition",
352
+ "stage",
353
+ "role",
354
+ "type",
355
+ "category",
356
+ "classification",
357
+ "priority",
358
+ "phase",
359
+ "mode",
360
+ "availability",
361
+ "outcome",
362
+ "result",
363
+ "health",
364
+ "progress",
365
+ "visibility",
366
+ "access",
367
+ "membership",
368
+ "sentiment",
369
+ "severity"
370
+ ]);
371
+ if (subject && stateEntry && typeof stateEntry[1] === "string") {
372
+ const details = Object.entries(obj).filter(([key, value]) => key !== stateEntry[0] && key !== subject.key && shouldDescribeSemantically(key, value)).map(([key, value]) => `${camelToWords(key)} ${formatSemanticValue(value)}`);
373
+ return `${subject.value} is ${stateEntry[1]}${details.length ? ` and has ${details.join(", ")}` : ""}.`;
374
+ }
375
+ return null;
376
+ }
377
+ function findSubject(obj) {
378
+ const subjectKeys = [
379
+ "name",
380
+ "title",
381
+ "label",
382
+ "displayName",
383
+ "entityName",
384
+ "fullName",
385
+ "userName",
386
+ "username",
387
+ "personName",
388
+ "customerName",
389
+ "clientName",
390
+ "ownerName",
391
+ "accountName",
392
+ "companyName",
393
+ "organizationName",
394
+ "teamName",
395
+ "departmentName",
396
+ "projectName",
397
+ "productName",
398
+ "serviceName",
399
+ "resourceName",
400
+ "fileName",
401
+ "deviceName",
402
+ "hostName",
403
+ "applicationName",
404
+ "appName",
405
+ "taskName",
406
+ "eventName",
407
+ "itemName",
408
+ "orderName",
409
+ "patientName",
410
+ "employeeName"
411
+ ];
412
+ const entries = Object.entries(obj);
413
+ for (const key of subjectKeys) {
414
+ if (typeof obj[key] === "string" && obj[key].trim()) {
415
+ return { key, value: obj[key].trim() };
416
+ }
417
+ }
418
+ const nameEntry = entries.find(([key, value]) => {
419
+ return key.toLowerCase().includes("name") && typeof value === "string" && value.trim().length > 0;
420
+ });
421
+ if (nameEntry) {
422
+ return { key: nameEntry[0], value: nameEntry[1].trim() };
423
+ }
424
+ return null;
425
+ }
426
+ function findSemanticEntry(obj, keys) {
427
+ const entries = Object.entries(obj);
428
+ const exactEntry = entries.find(([key]) => keys.includes(key));
429
+ if (exactEntry) return exactEntry;
430
+ return entries.find(([key]) => {
431
+ const normalizedKey = key.toLowerCase();
432
+ return keys.some((semanticKey) => normalizedKey.includes(semanticKey.toLowerCase()));
433
+ }) ?? null;
434
+ }
435
+ function shouldDescribeSemantically(key, value) {
436
+ return (typeof value === "string" || typeof value === "number" || typeof value === "boolean") && !["id", "timestamp", "createdAt", "updatedAt", "apiKey", "debug"].includes(key);
437
+ }
438
+ function formatSemanticValue(value) {
439
+ if (typeof value === "number") return formatNumber(value);
440
+ if (typeof value === "boolean") return value ? "enabled" : "disabled";
441
+ return String(value);
442
+ }
443
+ function describeClaim(claim) {
444
+ const parts = [];
445
+ if (typeof claim.status === "string") parts.push(`The claim is ${claim.status}`);
446
+ if (typeof claim.requestedAmount === "number") {
447
+ parts.push(`for ${formatNumber(claim.requestedAmount)}`);
448
+ }
449
+ if (typeof claim.claimNumber === "string") {
450
+ parts.push(`(reference ${claim.claimNumber})`);
451
+ }
452
+ return parts.length ? `${parts.join(" ")}.` : null;
453
+ }
454
+ function formatNumber(value) {
455
+ return new Intl.NumberFormat("en-US").format(value);
456
+ }
321
457
  function formatPropertyClause(key, value, depth) {
322
458
  const naturalKey = camelToWords(key);
323
459
  if (typeof value === "object" && value !== null && !Array.isArray(value)) {
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
1
  import {
2
2
  AIChain
3
- } from "./chunk-QHCUNOUH.js";
3
+ } from "./chunk-FBJ3DHS6.js";
4
4
  import {
5
5
  analyze
6
6
  } from "./chunk-2TDVL6BJ.js";
@@ -15,7 +15,7 @@ import {
15
15
  } from "./chunk-L7BC62MT.js";
16
16
  import {
17
17
  toNatural
18
- } from "./chunk-D4CFTFM3.js";
18
+ } from "./chunk-BN3L7TUV.js";
19
19
  import {
20
20
  prune
21
21
  } from "./chunk-ZD536GZF.js";
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@sohqureshi/tokenwise",
3
- "version": "1.0.9",
3
+ "version": "1.0.10",
4
4
  "description": "Optimize JSON data for AI by reducing token usage",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1,105 +0,0 @@
1
- // src/core/natural.ts
2
- function toNatural(data, depth = 0) {
3
- if (data === null || data === void 0) return "nothing";
4
- if (typeof data === "string") return data;
5
- if (typeof data === "number") return String(data);
6
- if (typeof data === "boolean") return data ? "yes" : "no";
7
- if (Array.isArray(data)) {
8
- if (data.length === 0) return "empty list";
9
- if (data.every(isPlainObject)) {
10
- return data.map((item, index) => `${index + 1}. ${stripTrailingPeriod(toNatural(item, depth + 1))}.`).join(" ");
11
- }
12
- const items = data.map((item) => toNatural(item, depth + 1));
13
- return joinNaturalList(items);
14
- }
15
- if (typeof data === "object") {
16
- return buildContextualStory(data, depth);
17
- }
18
- return String(data);
19
- }
20
- function buildContextualStory(obj, depth = 0) {
21
- let name = obj.name || obj.userName || obj.user;
22
- if (typeof name === "object" && name !== null && name.name) {
23
- name = name.name;
24
- }
25
- const entries = Object.entries(obj).filter(([key]) => {
26
- return !["id", "timestamp", "apiKey"].includes(key);
27
- });
28
- if (entries.length === 0) return "";
29
- let story = name && typeof name === "string" ? `User ${name}` : "";
30
- const clauses = entries.map(([key, value]) => {
31
- if (key === "user" && name && typeof value === "object" && value !== null && !Array.isArray(value)) {
32
- const userProps = Object.entries(value).filter(([k]) => !["id", "name", "timestamp", "apiKey"].includes(k)).map(([k, v]) => formatPropertyClause(k, v, depth + 1)).filter((c) => c !== null && c !== "").join(", ");
33
- return userProps ? `(${userProps})` : null;
34
- }
35
- return formatPropertyClause(key, value, depth);
36
- }).filter((c) => c !== null && c !== "");
37
- if (clauses.length === 0) return story;
38
- if (story) {
39
- story += " " + clauses.join(", ");
40
- } else {
41
- story = clauses.join(", ");
42
- }
43
- return story + ".";
44
- }
45
- function formatPropertyClause(key, value, depth) {
46
- const naturalKey = camelToWords(key);
47
- if (typeof value === "object" && value !== null && !Array.isArray(value)) {
48
- if (["internal", "metadata", "debug"].includes(key)) {
49
- return null;
50
- }
51
- const nested = Object.entries(value).filter(([k]) => !["id", "timestamp", "apiKey", "createdAt", "updatedAt"].includes(k)).map(([k, v]) => {
52
- if (typeof v === "object" && v !== null) {
53
- return formatPropertyClause(k, v, depth + 1);
54
- }
55
- const propValue = toNatural(v, depth + 1);
56
- if (propValue === "yes" || propValue === "no") {
57
- return `${camelToWords(k)} ${propValue === "yes" ? "enabled" : "disabled"}`;
58
- }
59
- return `${camelToWords(k)} ${propValue}`;
60
- }).filter((c) => c && c.trim()).join(", ");
61
- if (!nested) return null;
62
- return `${naturalKey}: ${nested}`;
63
- }
64
- if (Array.isArray(value)) {
65
- if (value.length === 0) return null;
66
- if (["skills", "hobbies", "interests", "tags", "languages", "items"].includes(key)) {
67
- const items2 = value.map((item) => {
68
- const str = String(item);
69
- return str.charAt(0).toUpperCase() + str.slice(1);
70
- });
71
- return `Having ${joinNaturalList(items2)}`;
72
- }
73
- const items = joinNaturalList(value.map((item) => toNatural(item, depth + 1)));
74
- return `${naturalKey}: ${items}`;
75
- }
76
- const naturalValue = toNatural(value, depth + 1);
77
- if (naturalValue === "yes") return `${naturalKey} enabled`;
78
- if (naturalValue === "no") return `${naturalKey} disabled`;
79
- if (key === "theme") return `prefers the ${naturalValue} ${key}`;
80
- if (key === "age") return `age: ${naturalValue}`;
81
- if (key === "email") return `email: ${naturalValue}`;
82
- if (key === "city") return `address: ${naturalValue}`;
83
- if (key === "debug" && naturalValue === "yes") return `debug enabled`;
84
- return `${naturalKey}: ${naturalValue}`;
85
- }
86
- function camelToWords(str) {
87
- return str.replace(/([A-Z])/g, " $1").toLowerCase().trim();
88
- }
89
- function isPlainObject(value) {
90
- return typeof value === "object" && value !== null && !Array.isArray(value);
91
- }
92
- function joinNaturalList(items) {
93
- if (items.length === 0) return "";
94
- if (items.length === 1) return items[0];
95
- if (items.length === 2) return `${items[0]} and ${items[1]}`;
96
- const lastItem = items[items.length - 1];
97
- return `${items.slice(0, -1).join(", ")} and ${lastItem}`;
98
- }
99
- function stripTrailingPeriod(value) {
100
- return value.replace(/\.+$/, "");
101
- }
102
-
103
- export {
104
- toNatural
105
- };