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.
@@ -0,0 +1,139 @@
1
+ # Architecture
2
+
3
+ Deep-dive into how json-humanized works internally.
4
+
5
+ ---
6
+
7
+ ## Data flow
8
+
9
+ ```
10
+ Input (file / string / value)
11
+
12
+
13
+ src/index.js ← Public API layer
14
+ humanize() / humanizeFile() / humanizeString()
15
+
16
+ ├─── engine: 'local' ──────────────────────────────────────────────────
17
+ │ │
18
+ │ ▼
19
+ │ src/humanizer.js
20
+ │ detectTopLevelShape(data) → What is this? (array, API resp, …)
21
+ │ buildIntro(data, shape) → Opening sentence
22
+ │ humanizeObject(data, depth) → Recursive field walk
23
+ │ │
24
+ │ ├─ humanizeKey(key) → "createdAt" → "created at"
25
+ │ ├─ detectKeyContext(key) → "email", "money", "datetime", …
26
+ │ └─ humanizeValue(v, key) → contextual formatting
27
+
28
+ └─── engine: 'ai' ─────────────────────────────────────────────────────
29
+
30
+
31
+ src/strategies/ai.js
32
+ Build system prompt + user message
33
+ Call Claude API (claude-opus-4-5)
34
+ Extract text from response blocks
35
+
36
+
37
+ Raw text string
38
+
39
+
40
+ src/formatters/index.js
41
+ applyFormat(text, format, meta)
42
+ → plain / markdown / story / json
43
+
44
+
45
+ Final string output
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Key design decisions
51
+
52
+ ### Why `commander` for CLI?
53
+
54
+ Commander is the most widely used Node.js CLI framework, well-maintained, and produces clean `--help` output with zero config. It handles argument parsing, option validation, and subcommand support if we ever need it.
55
+
56
+ ### Why `ora` for spinners?
57
+
58
+ Ora is the de facto standard for CLI spinners in Node.js. Version 5 is used (not 6+) because v6+ switched to pure ESM, which would require all consuming code to be ESM too. We stay at v5 to maintain CommonJS compatibility without bundling complexity.
59
+
60
+ ### Why `chalk@4` not `chalk@5`?
61
+
62
+ Same reason: chalk v5 is pure ESM. We use v4 for CommonJS compatibility.
63
+
64
+ ### Why no external test runner?
65
+
66
+ The test suite in `test/index.test.js` uses only Node.js built-ins. This means:
67
+ - Zero extra install time for contributors
68
+ - No Jest/Mocha configuration files
69
+ - Works on Node 14+
70
+ - `npm test` just works
71
+
72
+ ### Why optional dependency for `@anthropic-ai/sdk`?
73
+
74
+ Not all users need AI. Making it optional means:
75
+ - `npm install json-humanized` is fast (no heavy SDK)
76
+ - Users on restricted networks can use the local engine
77
+ - The package doesn't fail to install if the SDK has peer dep issues
78
+
79
+ The code in `src/strategies/ai.js` uses `require()` inside a try/catch and shows a helpful error message if the SDK isn't installed.
80
+
81
+ ### Why `humanizeObject` returns a string of newlines, not an array?
82
+
83
+ Early versions returned an array of sentences and joined at the top level. This was changed so that nested objects could naturally indent their output — an array of strings can't carry indentation context across recursion levels. Returning pre-indented strings makes the recursion simpler.
84
+
85
+ ---
86
+
87
+ ## Adding a new strategy
88
+
89
+ Create `src/strategies/my-strategy.js`:
90
+
91
+ ```javascript
92
+ 'use strict';
93
+
94
+ async function humanizeWithMyStrategy(data, options = {}) {
95
+ // ... your logic
96
+ return 'Human-readable string';
97
+ }
98
+
99
+ module.exports = { humanizeWithMyStrategy };
100
+ ```
101
+
102
+ Then register it in `src/index.js`:
103
+
104
+ ```javascript
105
+ const { humanizeWithMyStrategy } = require('./strategies/my-strategy');
106
+
107
+ // In humanize():
108
+ if (engine === 'my-strategy') {
109
+ text = await humanizeWithMyStrategy(data, options);
110
+ }
111
+ ```
112
+
113
+ Add it to the CLI validation:
114
+
115
+ ```javascript
116
+ const validEngines = ['local', 'ai', 'my-strategy'];
117
+ ```
118
+
119
+ ---
120
+
121
+ ## Field context detection priority
122
+
123
+ `detectKeyContext()` in `src/humanizer.js` tests conditions in this order:
124
+
125
+ 1. Exact match: `id`, `uuid`, `guid` → `identifier`
126
+ 2. Suffix match: `_id` → `reference`
127
+ 3. Datetime patterns: `created_at`, `timestamp`, etc.
128
+ 4. Communication: email, url, phone
129
+ 5. Financial: price, cost, amount, salary, etc.
130
+ 6. Counts and quantities
131
+ 7. Geographic: latitude, longitude
132
+ 8. Security: password, token, secret, key, hash
133
+ 9. Descriptive: name, title, description, bio, etc.
134
+ 10. Status, type, category
135
+ 11. Boolean flags: `is_*`, `has_*`, `enabled`, `active`
136
+ 12. Miscellaneous: age, version, color, rating, error
137
+ 13. Fallback: `generic`
138
+
139
+ The first matching condition wins.
package/docs/DEMO.html ADDED
@@ -0,0 +1,461 @@
1
+ <!DOCTYPE html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8">
5
+ <meta name="viewport" content="width=device-width, initial-scale=1.0">
6
+ <title>json-humanized — Live Demo</title>
7
+ <style>
8
+ @import url('https://fonts.googleapis.com/css2?family=JetBrains+Mono:wght@400;600&family=Syne:wght@400;700;800&display=swap');
9
+ :root {
10
+ --bg: #0d0d12;
11
+ --panel: #14141f;
12
+ --border: #252535;
13
+ --cyan: #00e5ff;
14
+ --green: #00ff9d;
15
+ --yellow: #ffd166;
16
+ --red: #ff6b6b;
17
+ --text: #e0e0f0;
18
+ --muted: #6060a0;
19
+ --radius: 12px;
20
+ }
21
+ * { box-sizing: border-box; margin: 0; padding: 0; }
22
+ body {
23
+ background: var(--bg);
24
+ color: var(--text);
25
+ font-family: 'Syne', sans-serif;
26
+ min-height: 100vh;
27
+ display: grid;
28
+ grid-template-rows: auto 1fr auto;
29
+ }
30
+ header {
31
+ padding: 32px 48px 24px;
32
+ border-bottom: 1px solid var(--border);
33
+ display: flex;
34
+ align-items: center;
35
+ gap: 20px;
36
+ }
37
+ header h1 {
38
+ font-size: 1.6rem;
39
+ font-weight: 800;
40
+ letter-spacing: -0.03em;
41
+ }
42
+ header h1 span { color: var(--cyan); }
43
+ header .badge {
44
+ font-family: 'JetBrains Mono', monospace;
45
+ font-size: 0.7rem;
46
+ background: var(--cyan)22;
47
+ color: var(--cyan);
48
+ border: 1px solid var(--cyan)44;
49
+ padding: 3px 10px;
50
+ border-radius: 99px;
51
+ }
52
+ header .links { margin-left: auto; display: flex; gap: 12px; }
53
+ header .links a {
54
+ font-size: 0.8rem;
55
+ color: var(--muted);
56
+ text-decoration: none;
57
+ transition: color .2s;
58
+ }
59
+ header .links a:hover { color: var(--cyan); }
60
+
61
+ main { display: grid; grid-template-columns: 1fr 1fr; gap: 0; }
62
+ .pane {
63
+ padding: 32px 40px;
64
+ display: flex;
65
+ flex-direction: column;
66
+ gap: 16px;
67
+ }
68
+ .pane + .pane { border-left: 1px solid var(--border); }
69
+ .pane-label {
70
+ font-size: 0.7rem;
71
+ font-weight: 700;
72
+ letter-spacing: .12em;
73
+ text-transform: uppercase;
74
+ color: var(--muted);
75
+ display: flex;
76
+ align-items: center;
77
+ gap: 8px;
78
+ }
79
+ .pane-label .dot {
80
+ width: 8px; height: 8px; border-radius: 50%;
81
+ }
82
+ .dot-cyan { background: var(--cyan); }
83
+ .dot-green { background: var(--green); }
84
+
85
+ textarea, #output {
86
+ flex: 1;
87
+ min-height: 340px;
88
+ font-family: 'JetBrains Mono', monospace;
89
+ font-size: 0.82rem;
90
+ line-height: 1.7;
91
+ background: var(--panel);
92
+ border: 1px solid var(--border);
93
+ border-radius: var(--radius);
94
+ padding: 20px;
95
+ color: var(--text);
96
+ resize: none;
97
+ outline: none;
98
+ transition: border-color .2s;
99
+ }
100
+ textarea:focus { border-color: var(--cyan)66; }
101
+ #output {
102
+ white-space: pre-wrap;
103
+ overflow-y: auto;
104
+ color: #c0c0e0;
105
+ }
106
+ #output.placeholder { color: var(--muted); font-style: italic; }
107
+ #output.loading { color: var(--cyan); animation: pulse 1s ease-in-out infinite; }
108
+ #output.error { color: var(--red); }
109
+ @keyframes pulse { 0%,100%{opacity:1} 50%{opacity:.5} }
110
+
111
+ .controls {
112
+ display: flex;
113
+ flex-wrap: wrap;
114
+ gap: 10px;
115
+ align-items: center;
116
+ }
117
+ select {
118
+ background: var(--panel);
119
+ border: 1px solid var(--border);
120
+ color: var(--text);
121
+ padding: 8px 12px;
122
+ border-radius: 8px;
123
+ font-family: 'Syne', sans-serif;
124
+ font-size: 0.8rem;
125
+ outline: none;
126
+ cursor: pointer;
127
+ transition: border-color .2s;
128
+ }
129
+ select:hover { border-color: var(--cyan)55; }
130
+
131
+ #run-btn {
132
+ margin-left: auto;
133
+ background: var(--cyan);
134
+ color: #000;
135
+ border: none;
136
+ padding: 10px 28px;
137
+ border-radius: 8px;
138
+ font-family: 'Syne', sans-serif;
139
+ font-weight: 700;
140
+ font-size: 0.85rem;
141
+ cursor: pointer;
142
+ transition: transform .15s, opacity .15s;
143
+ letter-spacing: .04em;
144
+ }
145
+ #run-btn:hover { opacity: .9; transform: translateY(-1px); }
146
+ #run-btn:active { transform: translateY(0); }
147
+ #run-btn:disabled { opacity: .5; cursor: not-allowed; transform: none; }
148
+
149
+ .samples {
150
+ display: flex;
151
+ gap: 8px;
152
+ flex-wrap: wrap;
153
+ }
154
+ .sample-btn {
155
+ background: transparent;
156
+ border: 1px solid var(--border);
157
+ color: var(--muted);
158
+ padding: 5px 12px;
159
+ border-radius: 6px;
160
+ font-family: 'JetBrains Mono', monospace;
161
+ font-size: 0.72rem;
162
+ cursor: pointer;
163
+ transition: all .2s;
164
+ }
165
+ .sample-btn:hover { border-color: var(--cyan)66; color: var(--cyan); }
166
+
167
+ footer {
168
+ padding: 16px 48px;
169
+ border-top: 1px solid var(--border);
170
+ font-size: 0.72rem;
171
+ color: var(--muted);
172
+ display: flex;
173
+ justify-content: space-between;
174
+ }
175
+ footer a { color: var(--muted); text-decoration: none; }
176
+ footer a:hover { color: var(--cyan); }
177
+
178
+ .info-bar {
179
+ font-family: 'JetBrains Mono', monospace;
180
+ font-size: 0.72rem;
181
+ color: var(--muted);
182
+ padding: 6px 12px;
183
+ background: var(--panel);
184
+ border: 1px solid var(--border);
185
+ border-radius: 6px;
186
+ display: flex;
187
+ gap: 16px;
188
+ }
189
+ .info-bar span.ok { color: var(--green); }
190
+ .info-bar span.bad { color: var(--red); }
191
+ </style>
192
+ </head>
193
+ <body>
194
+
195
+ <header>
196
+ <h1>json-<span>humanized</span></h1>
197
+ <span class="badge">v2.0</span>
198
+ <div class="links">
199
+ <a href="https://github.com/AceAnomDev/json-humanized" target="_blank">GitHub</a>
200
+ <a href="https://www.npmjs.com/package/json-humanized" target="_blank">npm</a>
201
+ <a href="https://github.com/AceAnomDev/json-humanized#readme" target="_blank">Docs</a>
202
+ </div>
203
+ </header>
204
+
205
+ <main>
206
+ <!-- LEFT: input -->
207
+ <div class="pane">
208
+ <div class="pane-label"><span class="dot dot-cyan"></span>JSON / YAML / TOML Input</div>
209
+
210
+ <div class="samples">
211
+ <span style="font-size:.72rem;color:var(--muted);margin-right:4px">Examples:</span>
212
+ <button class="sample-btn" onclick="loadSample('user')">user profile</button>
213
+ <button class="sample-btn" onclick="loadSample('api')">api response</button>
214
+ <button class="sample-btn" onclick="loadSample('error')">error</button>
215
+ <button class="sample-btn" onclick="loadSample('array')">list</button>
216
+ </div>
217
+
218
+ <textarea id="input" spellcheck="false" placeholder='Paste JSON, YAML, or TOML here…
219
+
220
+ {
221
+ "name": "Alice",
222
+ "age": 30
223
+ }'></textarea>
224
+
225
+ <div class="controls">
226
+ <select id="sel-engine">
227
+ <option value="local">⚡ local (offline)</option>
228
+ <option value="ai">🤖 ai (Claude API)</option>
229
+ </select>
230
+ <select id="sel-format">
231
+ <option value="plain">plain</option>
232
+ <option value="markdown">markdown</option>
233
+ <option value="story">story</option>
234
+ <option value="json">json</option>
235
+ </select>
236
+ <select id="sel-mode">
237
+ <option value="structured">structured</option>
238
+ <option value="prose">prose</option>
239
+ <option value="story">story</option>
240
+ </select>
241
+ <button id="run-btn" onclick="run()">Humanize →</button>
242
+ </div>
243
+ </div>
244
+
245
+ <!-- RIGHT: output -->
246
+ <div class="pane">
247
+ <div class="pane-label"><span class="dot dot-green"></span>Human-Readable Output</div>
248
+ <div id="output" class="placeholder">Your output will appear here…</div>
249
+ <div class="info-bar" id="info-bar">
250
+ <span id="stat-chars">chars: —</span>
251
+ <span id="stat-keys">keys: —</span>
252
+ <span id="stat-engine">engine: —</span>
253
+ <span id="stat-time">time: —</span>
254
+ </div>
255
+ </div>
256
+ </main>
257
+
258
+ <footer>
259
+ <span>json-humanized · MIT license</span>
260
+ <span><a href="https://github.com/AceAnomDev/json-humanized/issues" target="_blank">Report an issue</a></span>
261
+ </footer>
262
+
263
+ <script>
264
+ // ── samples ──────────────────────────────────────────────────────────────────
265
+ const SAMPLES = {
266
+ user: `{
267
+ "id": "usr_8f3k2",
268
+ "name": "Alice Johnson",
269
+ "email": "alice@example.com",
270
+ "age": 28,
271
+ "password": "hunter2",
272
+ "created_at": "2024-03-15T10:30:00Z",
273
+ "balance": 4250.75,
274
+ "is_active": true,
275
+ "address": {
276
+ "city": "San Francisco",
277
+ "country": "US",
278
+ "zip": "94105"
279
+ },
280
+ "tags": ["premium", "early-adopter"]
281
+ }`,
282
+ api: `{
283
+ "data": {
284
+ "results": [
285
+ { "id": 1, "title": "Introduction to AI", "score": 9.2 },
286
+ { "id": 2, "title": "Machine Learning 101", "score": 8.7 }
287
+ ],
288
+ "total": 42
289
+ },
290
+ "meta": {
291
+ "page": 1,
292
+ "per_page": 2,
293
+ "took_ms": 34
294
+ },
295
+ "links": {
296
+ "next": "/api/v1/courses?page=2"
297
+ }
298
+ }`,
299
+ error: `{
300
+ "error": {
301
+ "code": 422,
302
+ "message": "Validation failed",
303
+ "details": [
304
+ { "field": "email", "reason": "Invalid email format" },
305
+ { "field": "age", "reason": "Must be between 18 and 120" }
306
+ ]
307
+ },
308
+ "request_id": "req_7fh2k"
309
+ }`,
310
+ array: `[
311
+ { "user_id": "u1", "name": "Bob", "score": 98, "country": "UK" },
312
+ { "user_id": "u2", "name": "Carol", "score": 87, "country": "DE" },
313
+ { "user_id": "u3", "name": "Dave", "score": 73, "country": "US" }
314
+ ]`,
315
+ };
316
+
317
+ function loadSample(key) {
318
+ document.getElementById('input').value = SAMPLES[key];
319
+ }
320
+
321
+ // ── rule-based local engine (client-side) ───────────────────────────────────
322
+ function humanizeLocal(data, format) {
323
+ function fmtKey(k) {
324
+ return k.replace(/([A-Z])/g,' $1').replace(/[_\-]+/g,' ').trim().toLowerCase();
325
+ }
326
+ function fmtVal(v, key='') {
327
+ if (v === null || v === undefined) return 'not specified';
328
+ if (v === '') return 'empty';
329
+ const k = (key||'').toLowerCase();
330
+ if (typeof v === 'boolean') return v ? 'yes' : 'no';
331
+ if (/(password|secret|token|hash)/i.test(k)) return '*** (hidden)';
332
+ if (/(created|updated|at)$/i.test(k) && typeof v==='string') {
333
+ try { return new Date(v).toLocaleString('en-US',{year:'numeric',month:'long',day:'numeric',hour:'2-digit',minute:'2-digit'}); } catch{}
334
+ }
335
+ if (/(price|cost|amount|salary|balance)/i.test(k) && typeof v==='number') {
336
+ return v>=1e6?`$${(v/1e6).toFixed(2)}M`:v>=1e3?`$${(v/1e3).toFixed(1)}K`:`$${v.toFixed(2)}`;
337
+ }
338
+ if (/(email)/i.test(k)) return `email address: ${v}`;
339
+ if (typeof v === 'number') return v.toLocaleString();
340
+ if (typeof v === 'string' && v.length > 150) return v.slice(0,150)+'… (truncated)';
341
+ if (Array.isArray(v)) {
342
+ if (v.length===0) return 'none';
343
+ if (v.every(x=>typeof x !== 'object')) return v.join(', ');
344
+ return `${v.length} item(s)`;
345
+ }
346
+ if (typeof v === 'object') return `{${Object.keys(v).length} field(s)}`;
347
+ return String(v);
348
+ }
349
+ function objLines(obj, depth=0) {
350
+ const pfx = ' '.repeat(depth);
351
+ const lines = [];
352
+ for (const [k,v] of Object.entries(obj)) {
353
+ const label = fmtKey(k);
354
+ if (Array.isArray(v) && v.length && typeof v[0]==='object') {
355
+ lines.push(`${pfx}• ${cap(label)}: ${v.length} entr${v.length===1?'y':'ies'}`);
356
+ v.slice(0,3).forEach((it,i)=>{
357
+ lines.push(`${pfx} [${i+1}] ${objLines(it, depth+2).join(' ')}`);
358
+ });
359
+ if (v.length>3) lines.push(`${pfx} … and ${v.length-3} more`);
360
+ } else if (v && typeof v==='object' && !Array.isArray(v)) {
361
+ lines.push(`${pfx}• ${cap(label)}:`);
362
+ lines.push(...objLines(v, depth+1));
363
+ } else {
364
+ lines.push(`${pfx}• ${cap(label)}: ${fmtVal(v,k)}`);
365
+ }
366
+ }
367
+ return lines;
368
+ }
369
+ function cap(s) { return s ? s[0].toUpperCase()+s.slice(1) : ''; }
370
+
371
+ let intro='', body='';
372
+ if (Array.isArray(data)) {
373
+ intro = `This JSON contains a list of ${data.length} record${data.length!==1?'s':''}.`;
374
+ const limit = Math.min(data.length, 10);
375
+ const lines = [];
376
+ for(let i=0;i<limit;i++) {
377
+ if (typeof data[i]==='object') lines.push(`\nRecord ${i+1}:\n${objLines(data[i],1).join('\n')}`);
378
+ else lines.push(`\nItem ${i+1}: ${fmtVal(data[i])}`);
379
+ }
380
+ if (data.length>10) lines.push(`\n… and ${data.length-10} more records`);
381
+ body = lines.join('');
382
+ } else if (data && typeof data==='object') {
383
+ const keys = Object.keys(data);
384
+ if ('error' in data || 'errors' in data) intro='This JSON describes an error or failure response.';
385
+ else if ('data' in data && ('meta' in data || 'links' in data)) intro='This JSON is an API response with data and metadata.';
386
+ else intro=`This JSON contains a structured object with ${keys.length} field${keys.length!==1?'s':''}.`;
387
+ body = '\n' + objLines(data,0).join('\n');
388
+ } else {
389
+ return `This JSON contains a single ${typeof data} value: ${data}`;
390
+ }
391
+
392
+ const text = `${intro}\n${body}`;
393
+
394
+ if (format === 'markdown') {
395
+ return `# JSON Analysis\n\n## Summary\n\n${text}\n\n---\n*Processed by json-humanized (local engine)*`;
396
+ }
397
+ if (format === 'story') {
398
+ return `${'━'.repeat(50)}\n 📖 THE DATA STORY\n${'━'.repeat(50)}\n\n${text}\n\n${'━'.repeat(50)}`;
399
+ }
400
+ if (format === 'json') {
401
+ return JSON.stringify({ humanized: text, metadata: { engine: 'local', timestamp: new Date().toISOString() }}, null, 2);
402
+ }
403
+ return `${text}\n\n[Processed by: local]`;
404
+ }
405
+
406
+ // ── run ──────────────────────────────────────────────────────────────────────
407
+ async function run() {
408
+ const raw = document.getElementById('input').value.trim();
409
+ const engine = document.getElementById('sel-engine').value;
410
+ const format = document.getElementById('sel-format').value;
411
+ const out = document.getElementById('output');
412
+ const btn = document.getElementById('run-btn');
413
+
414
+ if (!raw) { out.textContent='⚠ Paste some JSON first'; out.className='error'; return; }
415
+
416
+ let data;
417
+ try { data = JSON.parse(raw); }
418
+ catch { out.textContent='✖ Invalid JSON — please check your input'; out.className='error'; return; }
419
+
420
+ // update info bar
421
+ const chars = raw.length;
422
+ const keys = Array.isArray(data) ? data.length : typeof data==='object' ? Object.keys(data).length : 1;
423
+ document.getElementById('stat-chars').textContent = `chars: ${chars}`;
424
+ document.getElementById('stat-keys').textContent = Array.isArray(data) ? `items: ${keys}` : `keys: ${keys}`;
425
+ document.getElementById('stat-engine').textContent = `engine: ${engine}`;
426
+
427
+ if (engine === 'ai') {
428
+ out.textContent = '⚠ AI mode is not available in the browser demo.\n\nInstall the CLI and use:\n jh data.json --engine ai';
429
+ out.className = 'error';
430
+ return;
431
+ }
432
+
433
+ btn.disabled = true;
434
+ out.className = 'loading';
435
+ out.textContent = 'Humanizing…';
436
+ const t0 = performance.now();
437
+
438
+ try {
439
+ const result = humanizeLocal(data, format);
440
+ const ms = (performance.now() - t0).toFixed(0);
441
+ out.textContent = result;
442
+ out.className = '';
443
+ document.getElementById('stat-time').innerHTML = `time: <span class="ok">${ms}ms</span>`;
444
+ } catch(e) {
445
+ out.textContent = '✖ ' + e.message;
446
+ out.className = 'error';
447
+ } finally {
448
+ btn.disabled = false;
449
+ }
450
+ }
451
+
452
+ // Auto-run on Ctrl+Enter
453
+ document.addEventListener('keydown', e => {
454
+ if ((e.ctrlKey || e.metaKey) && e.key === 'Enter') run();
455
+ });
456
+
457
+ // Load user sample on start
458
+ loadSample('user');
459
+ </script>
460
+ </body>
461
+ </html>