json-humanized 2.0.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/LICENSE +21 -0
- package/README.md +351 -0
- package/bin/cli.js +319 -0
- package/docs/ARCHITECTURE.md +139 -0
- package/docs/DEMO.html +461 -0
- package/docs/PUBLISHING.md +124 -0
- package/examples/api-response.json +42 -0
- package/examples/demo.js +50 -0
- package/examples/user-profile.json +36 -0
- package/index.d.ts +138 -0
- package/package.json +71 -0
- package/src/cache.js +172 -0
- package/src/config.js +259 -0
- package/src/diff.js +284 -0
- package/src/formatters/index.js +113 -0
- package/src/formatters/template.js +132 -0
- package/src/humanizer.js +307 -0
- package/src/index.js +157 -0
- package/src/parsers/index.js +119 -0
- package/src/strategies/ai.js +108 -0
- package/src/strategies/ollama.js +135 -0
- package/src/strategies/openai.js +82 -0
- package/src/watch.js +133 -0
|
@@ -0,0 +1,132 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* formatters/template.js — render humanized output via a custom Handlebars template.
|
|
5
|
+
*
|
|
6
|
+
* Requires: npm install handlebars
|
|
7
|
+
*
|
|
8
|
+
* Template variables available:
|
|
9
|
+
* {{humanized}} — the humanized text string
|
|
10
|
+
* {{filename}} — source filename
|
|
11
|
+
* {{engine}} — 'local' or 'ai'
|
|
12
|
+
* {{format}} — chosen output format
|
|
13
|
+
* {{timestamp}} — ISO timestamp
|
|
14
|
+
* {{stats.type}} — 'Object' | 'Array' | 'Primitive'
|
|
15
|
+
* {{stats.keys}} — number of top-level keys (objects)
|
|
16
|
+
* {{stats.items}} — number of items (arrays)
|
|
17
|
+
*
|
|
18
|
+
* Example template (report.hbs):
|
|
19
|
+
* # Report: {{filename}}
|
|
20
|
+
* Generated: {{timestamp}}
|
|
21
|
+
*
|
|
22
|
+
* {{humanized}}
|
|
23
|
+
*
|
|
24
|
+
* ---
|
|
25
|
+
* Engine: {{engine}} | Keys: {{stats.keys}}
|
|
26
|
+
*/
|
|
27
|
+
|
|
28
|
+
const fs = require('fs');
|
|
29
|
+
const path = require('path');
|
|
30
|
+
|
|
31
|
+
function requireHandlebars() {
|
|
32
|
+
try {
|
|
33
|
+
return require('handlebars');
|
|
34
|
+
} catch {
|
|
35
|
+
throw new Error(
|
|
36
|
+
'Handlebars is required for template output.\n' +
|
|
37
|
+
'Install it with: npm install handlebars'
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Compute basic stats about a parsed JSON value.
|
|
44
|
+
*/
|
|
45
|
+
function computeStats(data) {
|
|
46
|
+
if (data === null || data === undefined) {
|
|
47
|
+
return { type: 'Primitive', keys: 0, items: 0 };
|
|
48
|
+
}
|
|
49
|
+
if (Array.isArray(data)) {
|
|
50
|
+
return { type: 'Array', keys: 0, items: data.length };
|
|
51
|
+
}
|
|
52
|
+
if (typeof data === 'object') {
|
|
53
|
+
return { type: 'Object', keys: Object.keys(data).length, items: 0 };
|
|
54
|
+
}
|
|
55
|
+
return { type: 'Primitive', keys: 0, items: 0 };
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Render using a Handlebars template file.
|
|
60
|
+
*
|
|
61
|
+
* @param {string} humanizedText output from the humanizer / AI engine
|
|
62
|
+
* @param {string} templatePath path to a .hbs file
|
|
63
|
+
* @param {object} context extra context merged into template vars
|
|
64
|
+
* @returns {string} rendered output
|
|
65
|
+
*/
|
|
66
|
+
function renderTemplate(humanizedText, templatePath, context = {}) {
|
|
67
|
+
const Handlebars = requireHandlebars();
|
|
68
|
+
|
|
69
|
+
const absPath = path.resolve(templatePath);
|
|
70
|
+
if (!fs.existsSync(absPath)) {
|
|
71
|
+
throw new Error(`Template file not found: ${absPath}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const source = fs.readFileSync(absPath, 'utf8');
|
|
75
|
+
const template = Handlebars.compile(source);
|
|
76
|
+
|
|
77
|
+
const vars = {
|
|
78
|
+
humanized: humanizedText,
|
|
79
|
+
filename: context.filename || '',
|
|
80
|
+
engine: context.engine || 'local',
|
|
81
|
+
format: context.format || 'plain',
|
|
82
|
+
timestamp: new Date().toISOString(),
|
|
83
|
+
stats: computeStats(context.data),
|
|
84
|
+
...context,
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
return template(vars);
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/**
|
|
91
|
+
* Render using an inline Handlebars template string.
|
|
92
|
+
*
|
|
93
|
+
* @param {string} humanizedText
|
|
94
|
+
* @param {string} templateString Handlebars template source
|
|
95
|
+
* @param {object} context
|
|
96
|
+
* @returns {string}
|
|
97
|
+
*/
|
|
98
|
+
function renderTemplateString(humanizedText, templateString, context = {}) {
|
|
99
|
+
const Handlebars = requireHandlebars();
|
|
100
|
+
|
|
101
|
+
const template = Handlebars.compile(templateString);
|
|
102
|
+
|
|
103
|
+
const vars = {
|
|
104
|
+
humanized: humanizedText,
|
|
105
|
+
filename: context.filename || '',
|
|
106
|
+
engine: context.engine || 'local',
|
|
107
|
+
format: context.format || 'plain',
|
|
108
|
+
timestamp: new Date().toISOString(),
|
|
109
|
+
stats: computeStats(context.data),
|
|
110
|
+
...context,
|
|
111
|
+
};
|
|
112
|
+
|
|
113
|
+
return template(vars);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Generate an example .hbs template file at the given path.
|
|
118
|
+
*/
|
|
119
|
+
function generateExampleTemplate(outputPath) {
|
|
120
|
+
const example = `# {{filename}}
|
|
121
|
+
> Generated: {{timestamp}} · Engine: {{engine}}
|
|
122
|
+
|
|
123
|
+
{{humanized}}
|
|
124
|
+
|
|
125
|
+
---
|
|
126
|
+
*Stats: {{stats.type}}{{#if stats.keys}}, {{stats.keys}} keys{{/if}}{{#if stats.items}}, {{stats.items}} items{{/if}}*
|
|
127
|
+
`;
|
|
128
|
+
fs.writeFileSync(outputPath, example, 'utf8');
|
|
129
|
+
return outputPath;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
module.exports = { renderTemplate, renderTemplateString, generateExampleTemplate };
|
package/src/humanizer.js
ADDED
|
@@ -0,0 +1,307 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// json-humanized · Rule-based humanization engine
|
|
5
|
+
// Works 100% offline, no API key needed
|
|
6
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
+
|
|
8
|
+
const TYPE_LABELS = {
|
|
9
|
+
string: 'text value',
|
|
10
|
+
number: 'number',
|
|
11
|
+
boolean: 'flag',
|
|
12
|
+
object: 'object',
|
|
13
|
+
array: 'list',
|
|
14
|
+
null: 'empty value',
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Detect semantic meaning from a key name
|
|
19
|
+
*/
|
|
20
|
+
function detectKeyContext(key) {
|
|
21
|
+
const k = key.toLowerCase();
|
|
22
|
+
|
|
23
|
+
if (/^(id|uuid|guid|_id)$/.test(k)) return 'identifier';
|
|
24
|
+
if (/(_id|Id)$/.test(key)) return 'reference';
|
|
25
|
+
if (/(created|updated|modified|timestamp|date|time|at)$/i.test(k)) return 'datetime';
|
|
26
|
+
if (/(email|mail)$/i.test(k)) return 'email';
|
|
27
|
+
if (/(url|uri|href|link|website)$/i.test(k)) return 'url';
|
|
28
|
+
if (/(phone|tel|mobile|fax)$/i.test(k)) return 'phone';
|
|
29
|
+
if (/(price|cost|amount|salary|wage|fee|balance)/i.test(k)) return 'money';
|
|
30
|
+
if (/(count|qty|quantity|num|total|sum|size|length)/i.test(k)) return 'count';
|
|
31
|
+
if (/(lat|latitude)/i.test(k)) return 'latitude';
|
|
32
|
+
if (/(lon|lng|longitude)/i.test(k)) return 'longitude';
|
|
33
|
+
if (/(password|secret|token|key|hash)/i.test(k)) return 'sensitive';
|
|
34
|
+
if (/(name|title|label|heading|subject)/i.test(k)) return 'name';
|
|
35
|
+
if (/(desc|description|about|bio|summary|notes|comment|text|body|content)/i.test(k)) return 'description';
|
|
36
|
+
if (/(status|state|phase|stage)/i.test(k)) return 'status';
|
|
37
|
+
if (/(type|kind|category|group|tag|class)/i.test(k)) return 'category';
|
|
38
|
+
if (/(enabled|active|visible|public|is_|has_)/i.test(k)) return 'boolean-flag';
|
|
39
|
+
if (/(^age$|_age$|^age_)/i.test(k)) return 'age';
|
|
40
|
+
if (/(version|ver)/i.test(k)) return 'version';
|
|
41
|
+
if (/(color|colour)/i.test(k)) return 'color';
|
|
42
|
+
if (/(address|city|country|region|zip|postal)/i.test(k)) return 'location';
|
|
43
|
+
if (/(rating|score|rank)/i.test(k)) return 'rating';
|
|
44
|
+
if (/(error|err|exception|message|msg)/i.test(k)) return 'error';
|
|
45
|
+
|
|
46
|
+
return 'generic';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Format a key name into natural English label
|
|
51
|
+
*/
|
|
52
|
+
function humanizeKey(key) {
|
|
53
|
+
return key
|
|
54
|
+
.replace(/([A-Z])/g, ' $1') // camelCase → spaced
|
|
55
|
+
.replace(/[_\-\.]+/g, ' ') // snake_case / kebab-case → spaced
|
|
56
|
+
.replace(/\s+/g, ' ')
|
|
57
|
+
.trim()
|
|
58
|
+
.toLowerCase();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Format a value with contextual awareness
|
|
63
|
+
*/
|
|
64
|
+
function humanizeValue(value, key = '', depth = 0) {
|
|
65
|
+
if (value === null || value === undefined) return 'not specified';
|
|
66
|
+
if (value === '') return 'empty';
|
|
67
|
+
|
|
68
|
+
const ctx = detectKeyContext(key);
|
|
69
|
+
const type = typeof value;
|
|
70
|
+
|
|
71
|
+
if (type === 'boolean') {
|
|
72
|
+
if (ctx === 'boolean-flag') {
|
|
73
|
+
const label = humanizeKey(key);
|
|
74
|
+
return value ? `yes (${label} is active)` : `no (${label} is inactive)`;
|
|
75
|
+
}
|
|
76
|
+
return value ? 'yes' : 'no';
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (type === 'number') {
|
|
80
|
+
if (ctx === 'money') return formatMoney(value);
|
|
81
|
+
if (ctx === 'count') return `${value.toLocaleString()} item${value !== 1 ? 's' : ''}`;
|
|
82
|
+
if (ctx === 'rating') return `${value} out of 10`;
|
|
83
|
+
if (ctx === 'age') return `${value} year${value !== 1 ? 's' : ''} old`;
|
|
84
|
+
if (ctx === 'latitude') return `${Math.abs(value)}° ${value >= 0 ? 'N' : 'S'}`;
|
|
85
|
+
if (ctx === 'longitude')return `${Math.abs(value)}° ${value >= 0 ? 'E' : 'W'}`;
|
|
86
|
+
return value.toLocaleString();
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
if (type === 'string') {
|
|
90
|
+
if (ctx === 'sensitive') return '*** (hidden for security)';
|
|
91
|
+
if (ctx === 'datetime') return formatDatetime(value);
|
|
92
|
+
if (ctx === 'email') return `email address: ${value}`;
|
|
93
|
+
if (ctx === 'url') return `link: ${value}`;
|
|
94
|
+
if (ctx === 'phone') return `phone: ${value}`;
|
|
95
|
+
if (ctx === 'color') return `color ${value}`;
|
|
96
|
+
if (ctx === 'version') return `version ${value}`;
|
|
97
|
+
if (value.length > 200) return `${value.slice(0, 200)}… (truncated, ${value.length} characters total)`;
|
|
98
|
+
return value;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (Array.isArray(value)) {
|
|
102
|
+
return humanizeArray(value, key, depth);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
if (type === 'object') {
|
|
106
|
+
return humanizeObject(value, depth + 1);
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return String(value);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function formatMoney(n) {
|
|
113
|
+
if (n >= 1_000_000) return `$${(n / 1_000_000).toFixed(2)}M`;
|
|
114
|
+
if (n >= 1_000) return `$${(n / 1_000).toFixed(1)}K`;
|
|
115
|
+
return `$${n.toFixed(2)}`;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function formatDatetime(str) {
|
|
119
|
+
try {
|
|
120
|
+
const d = new Date(str);
|
|
121
|
+
if (isNaN(d.getTime())) return str;
|
|
122
|
+
return d.toLocaleString('en-US', {
|
|
123
|
+
year: 'numeric', month: 'long', day: 'numeric',
|
|
124
|
+
hour: '2-digit', minute: '2-digit',
|
|
125
|
+
});
|
|
126
|
+
} catch {
|
|
127
|
+
return str;
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Humanize an array value
|
|
133
|
+
*/
|
|
134
|
+
function humanizeArray(arr, key = '', depth = 0) {
|
|
135
|
+
if (arr.length === 0) return 'an empty list';
|
|
136
|
+
|
|
137
|
+
const itemType = typeof arr[0];
|
|
138
|
+
const allSameType = arr.every(i => typeof i === itemType);
|
|
139
|
+
const label = humanizeKey(key) || 'items';
|
|
140
|
+
|
|
141
|
+
// Simple scalar arrays → inline sentence
|
|
142
|
+
if (allSameType && ['string', 'number', 'boolean'].includes(itemType) && arr.length <= 8) {
|
|
143
|
+
const formatted = arr.map(v => humanizeValue(v, '', depth));
|
|
144
|
+
if (formatted.length === 1) return formatted[0];
|
|
145
|
+
const last = formatted.pop();
|
|
146
|
+
return `${formatted.join(', ')} and ${last}`;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
// Complex arrays → count + describe first element
|
|
150
|
+
const preview = humanizeValue(arr[0], '', depth + 1);
|
|
151
|
+
return `a collection of ${arr.length} ${label} (e.g. ${preview}${arr.length > 1 ? ', and more' : ''})`;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Humanize an object recursively, returns array of sentences
|
|
156
|
+
*/
|
|
157
|
+
function humanizeObject(obj, depth = 0) {
|
|
158
|
+
const entries = Object.entries(obj).filter(([, v]) => v !== undefined);
|
|
159
|
+
if (entries.length === 0) return 'an empty object';
|
|
160
|
+
|
|
161
|
+
const sentences = [];
|
|
162
|
+
const indent = ' '.repeat(depth);
|
|
163
|
+
|
|
164
|
+
for (const [key, value] of entries) {
|
|
165
|
+
const label = humanizeKey(key);
|
|
166
|
+
const ctx = detectKeyContext(key);
|
|
167
|
+
|
|
168
|
+
if (Array.isArray(value)) {
|
|
169
|
+
if (value.length === 0) {
|
|
170
|
+
sentences.push(`${indent}• ${capitalize(label)}: none`);
|
|
171
|
+
} else if (isScalarArray(value)) {
|
|
172
|
+
sentences.push(`${indent}• ${capitalize(label)}: ${humanizeArray(value, key, depth)}`);
|
|
173
|
+
} else {
|
|
174
|
+
sentences.push(`${indent}• ${capitalize(label)}: ${value.length} entr${value.length === 1 ? 'y' : 'ies'}`);
|
|
175
|
+
value.slice(0, 5).forEach((item, i) => {
|
|
176
|
+
if (typeof item === 'object' && item !== null) {
|
|
177
|
+
const sub = humanizeObject(item, depth + 1);
|
|
178
|
+
sentences.push(`${indent} [${i + 1}] ${sub}`);
|
|
179
|
+
} else {
|
|
180
|
+
sentences.push(`${indent} [${i + 1}] ${humanizeValue(item, '', depth + 1)}`);
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
if (value.length > 5) {
|
|
184
|
+
sentences.push(`${indent} … and ${value.length - 5} more`);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
} else if (typeof value === 'object' && value !== null) {
|
|
188
|
+
sentences.push(`${indent}• ${capitalize(label)}:`);
|
|
189
|
+
const sub = humanizeObject(value, depth + 1);
|
|
190
|
+
sentences.push(sub);
|
|
191
|
+
} else {
|
|
192
|
+
const hval = humanizeValue(value, key, depth);
|
|
193
|
+
if (ctx === 'identifier') {
|
|
194
|
+
sentences.push(`${indent}• Identifier: ${hval}`);
|
|
195
|
+
} else if (ctx === 'description') {
|
|
196
|
+
sentences.push(`${indent}• ${capitalize(label)}: "${hval}"`);
|
|
197
|
+
} else {
|
|
198
|
+
sentences.push(`${indent}• ${capitalize(label)}: ${hval}`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
return sentences.join('\n');
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function isScalarArray(arr) {
|
|
207
|
+
return arr.every(i => typeof i !== 'object' || i === null);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function capitalize(str) {
|
|
211
|
+
if (!str) return '';
|
|
212
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
216
|
+
// Top-level entry point
|
|
217
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Detect what kind of structure this JSON represents
|
|
221
|
+
*/
|
|
222
|
+
function detectTopLevelShape(data) {
|
|
223
|
+
if (Array.isArray(data)) {
|
|
224
|
+
if (data.length === 0) return { shape: 'empty-array' };
|
|
225
|
+
const first = data[0];
|
|
226
|
+
if (typeof first === 'object' && first !== null) return { shape: 'record-list', count: data.length };
|
|
227
|
+
return { shape: 'scalar-list', count: data.length };
|
|
228
|
+
}
|
|
229
|
+
if (typeof data === 'object' && data !== null) {
|
|
230
|
+
const keys = Object.keys(data);
|
|
231
|
+
// Detect common API shapes
|
|
232
|
+
if ('data' in data && ('meta' in data || 'links' in data || 'pagination' in data)) return { shape: 'api-response' };
|
|
233
|
+
if ('error' in data || 'errors' in data || 'message' in data && 'code' in data) return { shape: 'error-response' };
|
|
234
|
+
if ('users' in data || 'items' in data || 'results' in data || 'records' in data) return { shape: 'collection' };
|
|
235
|
+
if (keys.length <= 2) return { shape: 'simple-object' };
|
|
236
|
+
return { shape: 'complex-object', keys: keys.length };
|
|
237
|
+
}
|
|
238
|
+
return { shape: 'primitive' };
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
/**
|
|
242
|
+
* Build a natural-language introduction for the top-level shape
|
|
243
|
+
*/
|
|
244
|
+
function buildIntro(data, shape) {
|
|
245
|
+
switch (shape.shape) {
|
|
246
|
+
case 'empty-array':
|
|
247
|
+
return 'This JSON contains an empty list with no items.';
|
|
248
|
+
case 'record-list':
|
|
249
|
+
return `This JSON contains a list of ${shape.count} record${shape.count !== 1 ? 's' : ''}.`;
|
|
250
|
+
case 'scalar-list':
|
|
251
|
+
return `This JSON contains a list of ${shape.count} value${shape.count !== 1 ? 's' : ''}.`;
|
|
252
|
+
case 'api-response':
|
|
253
|
+
return 'This JSON is an API response with data and metadata.';
|
|
254
|
+
case 'error-response':
|
|
255
|
+
return 'This JSON describes an error or failure response.';
|
|
256
|
+
case 'collection':
|
|
257
|
+
return 'This JSON contains a collection of resources.';
|
|
258
|
+
case 'simple-object':
|
|
259
|
+
return 'This JSON contains a simple object with a few fields.';
|
|
260
|
+
case 'complex-object':
|
|
261
|
+
return `This JSON contains a structured object with ${shape.keys} fields.`;
|
|
262
|
+
case 'primitive':
|
|
263
|
+
return `This JSON contains a single value: ${String(data)}.`;
|
|
264
|
+
default:
|
|
265
|
+
return 'This JSON contains the following data:';
|
|
266
|
+
}
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
/**
|
|
270
|
+
* Generate a human-readable summary using the rule-based engine
|
|
271
|
+
*/
|
|
272
|
+
function humanizeLocal(data, options = {}) {
|
|
273
|
+
const { mode = 'structured', maxDepth = 10 } = options;
|
|
274
|
+
const shape = detectTopLevelShape(data);
|
|
275
|
+
const intro = buildIntro(data, shape);
|
|
276
|
+
|
|
277
|
+
if (shape.shape === 'primitive') {
|
|
278
|
+
return intro;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
let body = '';
|
|
282
|
+
|
|
283
|
+
if (Array.isArray(data)) {
|
|
284
|
+
if (isScalarArray(data)) {
|
|
285
|
+
body = `\nValues: ${humanizeArray(data, 'items', 0)}`;
|
|
286
|
+
} else {
|
|
287
|
+
const lines = [];
|
|
288
|
+
const limit = Math.min(data.length, 20);
|
|
289
|
+
for (let i = 0; i < limit; i++) {
|
|
290
|
+
const item = data[i];
|
|
291
|
+
if (typeof item === 'object' && item !== null) {
|
|
292
|
+
lines.push(`\nRecord ${i + 1}:\n${humanizeObject(item, 1)}`);
|
|
293
|
+
} else {
|
|
294
|
+
lines.push(`\nItem ${i + 1}: ${humanizeValue(item)}`);
|
|
295
|
+
}
|
|
296
|
+
}
|
|
297
|
+
if (data.length > 20) lines.push(`\n… and ${data.length - 20} more records`);
|
|
298
|
+
body = lines.join('');
|
|
299
|
+
}
|
|
300
|
+
} else {
|
|
301
|
+
body = '\n' + humanizeObject(data, 0);
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
return `${intro}\n${body}`;
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
module.exports = { humanizeLocal, humanizeKey, humanizeValue, detectKeyContext };
|
package/src/index.js
ADDED
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
4
|
+
// json-humanized · Main public API (v2.0)
|
|
5
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
6
|
+
|
|
7
|
+
const fs = require('fs');
|
|
8
|
+
const path = require('path');
|
|
9
|
+
|
|
10
|
+
const { humanizeLocal } = require('./humanizer');
|
|
11
|
+
const { humanizeWithAI } = require('./strategies/ai');
|
|
12
|
+
const { applyFormat } = require('./formatters');
|
|
13
|
+
const { withCache } = require('./cache');
|
|
14
|
+
const { loadConfig } = require('./config');
|
|
15
|
+
const { parseAny, parseFile } = require('./parsers');
|
|
16
|
+
|
|
17
|
+
// ─── stats helper ────────────────────────────────────────────────────────────
|
|
18
|
+
|
|
19
|
+
function computeStats(data) {
|
|
20
|
+
const stats = {};
|
|
21
|
+
if (Array.isArray(data)) {
|
|
22
|
+
stats['Type'] = 'Array';
|
|
23
|
+
stats['Total items'] = data.length;
|
|
24
|
+
if (data.length > 0 && typeof data[0] === 'object') {
|
|
25
|
+
stats['Keys per record'] = Object.keys(data[0] || {}).length;
|
|
26
|
+
}
|
|
27
|
+
} else if (typeof data === 'object' && data !== null) {
|
|
28
|
+
stats['Type'] = 'Object';
|
|
29
|
+
stats['Top-level keys'] = Object.keys(data).length;
|
|
30
|
+
const nested = Object.values(data).filter(v => typeof v === 'object' && v !== null).length;
|
|
31
|
+
if (nested > 0) stats['Nested objects'] = nested;
|
|
32
|
+
} else {
|
|
33
|
+
stats['Type'] = typeof data;
|
|
34
|
+
stats['Value'] = String(data).slice(0, 50);
|
|
35
|
+
}
|
|
36
|
+
return stats;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// ─── core humanize ───────────────────────────────────────────────────────────
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Core humanize function — works on already-parsed data.
|
|
43
|
+
*
|
|
44
|
+
* @param {any} data
|
|
45
|
+
* @param {object} [options]
|
|
46
|
+
* @param {'local'|'ai'} [options.engine='local']
|
|
47
|
+
* @param {'anthropic'|'openai'|'ollama'} [options.aiProvider='anthropic']
|
|
48
|
+
* @param {string} [options.apiKey]
|
|
49
|
+
* @param {'plain'|'markdown'|'story'|'json'} [options.format='plain']
|
|
50
|
+
* @param {'prose'|'structured'|'story'} [options.mode='structured']
|
|
51
|
+
* @param {string} [options.lang='English']
|
|
52
|
+
* @param {string} [options.context='']
|
|
53
|
+
* @param {string} [options.filename='']
|
|
54
|
+
* @param {number} [options.maxChars=12000]
|
|
55
|
+
* @param {string} [options.template] path to .hbs template
|
|
56
|
+
* @param {boolean} [options.cache=true]
|
|
57
|
+
* @param {number} [options.cacheTTL=3600]
|
|
58
|
+
* @param {string} [options.configPath] explicit config file
|
|
59
|
+
* @returns {Promise<string>}
|
|
60
|
+
*/
|
|
61
|
+
async function humanize(data, options = {}) {
|
|
62
|
+
// Merge config file options (if any) — CLI/API options take precedence
|
|
63
|
+
const { config } = loadConfig(options.configPath);
|
|
64
|
+
const merged = { ...config, ...options };
|
|
65
|
+
|
|
66
|
+
const {
|
|
67
|
+
engine = 'local',
|
|
68
|
+
aiProvider = 'anthropic',
|
|
69
|
+
apiKey = process.env.ANTHROPIC_API_KEY,
|
|
70
|
+
format = 'plain',
|
|
71
|
+
mode = 'structured',
|
|
72
|
+
lang = 'English',
|
|
73
|
+
context = '',
|
|
74
|
+
filename = '',
|
|
75
|
+
maxChars = 12000,
|
|
76
|
+
template,
|
|
77
|
+
cache = true,
|
|
78
|
+
cacheTTL = 3600,
|
|
79
|
+
} = merged;
|
|
80
|
+
|
|
81
|
+
const engineFn = async () => {
|
|
82
|
+
if (engine === 'ai') {
|
|
83
|
+
return humanizeWithAI(data, { apiKey, aiProvider, mode, lang, context, maxChars, ...merged });
|
|
84
|
+
}
|
|
85
|
+
return humanizeLocal(data, { mode });
|
|
86
|
+
};
|
|
87
|
+
|
|
88
|
+
// Use cache only for AI engine (local is instant)
|
|
89
|
+
const text = engine === 'ai'
|
|
90
|
+
? await withCache(data, { engine, aiProvider, format, lang, context, cacheTTL }, engineFn, cache)
|
|
91
|
+
: await engineFn();
|
|
92
|
+
|
|
93
|
+
const stats = computeStats(data);
|
|
94
|
+
const meta = { engine, aiProvider, filename, timestamp: new Date().toISOString(), stats, data };
|
|
95
|
+
|
|
96
|
+
// Template output takes priority over standard formatters
|
|
97
|
+
if (template) {
|
|
98
|
+
const { renderTemplate } = require('./formatters/template');
|
|
99
|
+
return renderTemplate(text, template, meta);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
return applyFormat(text, format, meta);
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
// ─── file humanizer ──────────────────────────────────────────────────────────
|
|
106
|
+
|
|
107
|
+
/**
|
|
108
|
+
* Humanize a JSON/YAML/TOML file from disk.
|
|
109
|
+
*
|
|
110
|
+
* @param {string} filePath
|
|
111
|
+
* @param {object} [options] same as humanize()
|
|
112
|
+
*/
|
|
113
|
+
async function humanizeFile(filePath, options = {}) {
|
|
114
|
+
const resolved = path.resolve(filePath);
|
|
115
|
+
|
|
116
|
+
if (!fs.existsSync(resolved)) {
|
|
117
|
+
throw new Error(`File not found: ${resolved}`);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
const data = parseFile(resolved);
|
|
121
|
+
|
|
122
|
+
return humanize(data, {
|
|
123
|
+
...options,
|
|
124
|
+
filename: options.filename || path.basename(resolved),
|
|
125
|
+
});
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// ─── string humanizer ────────────────────────────────────────────────────────
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Humanize a raw JSON/YAML/TOML string.
|
|
132
|
+
*
|
|
133
|
+
* @param {string} rawString
|
|
134
|
+
* @param {object} [options]
|
|
135
|
+
*/
|
|
136
|
+
async function humanizeString(rawString, options = {}) {
|
|
137
|
+
let data;
|
|
138
|
+
try {
|
|
139
|
+
data = parseAny(rawString, options.filename || '');
|
|
140
|
+
} catch (err) {
|
|
141
|
+
throw new Error(`Invalid input: ${err.message}`);
|
|
142
|
+
}
|
|
143
|
+
return humanize(data, options);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── exports ─────────────────────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
module.exports = {
|
|
149
|
+
humanize,
|
|
150
|
+
humanizeFile,
|
|
151
|
+
humanizeString,
|
|
152
|
+
// Re-export sub-modules for power users
|
|
153
|
+
diff: require('./diff'),
|
|
154
|
+
cache: require('./cache'),
|
|
155
|
+
config: require('./config'),
|
|
156
|
+
watch: require('./watch'),
|
|
157
|
+
};
|