claude-inspect 0.1.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.
Files changed (45) hide show
  1. package/LICENSE +21 -0
  2. package/README.de.md +84 -0
  3. package/README.md +167 -0
  4. package/dist/app/server/cli/demo.js +321 -0
  5. package/dist/app/server/cli/service.js +170 -0
  6. package/dist/app/server/cli-snapshot.js +34 -0
  7. package/dist/app/server/cli.js +84 -0
  8. package/dist/app/server/config.js +41 -0
  9. package/dist/app/server/formats/history/prompt.js +17 -0
  10. package/dist/app/server/formats/index.js +35 -0
  11. package/dist/app/server/formats/job/job.js +132 -0
  12. package/dist/app/server/formats/process/session-file.js +77 -0
  13. package/dist/app/server/formats/registry.js +87 -0
  14. package/dist/app/server/formats/subagent/meta.js +28 -0
  15. package/dist/app/server/formats/task/task.js +33 -0
  16. package/dist/app/server/formats/transcript/common.js +33 -0
  17. package/dist/app/server/formats/transcript/message.js +206 -0
  18. package/dist/app/server/formats/transcript/other.js +130 -0
  19. package/dist/app/server/formats/types.js +10 -0
  20. package/dist/app/server/formats/util.js +22 -0
  21. package/dist/app/server/history/aggregator.js +318 -0
  22. package/dist/app/server/history/search.js +138 -0
  23. package/dist/app/server/http.js +169 -0
  24. package/dist/app/server/inspector.js +468 -0
  25. package/dist/app/server/main.js +2 -0
  26. package/dist/app/server/model/analyze.js +112 -0
  27. package/dist/app/server/model/flow.js +47 -0
  28. package/dist/app/server/run.js +37 -0
  29. package/dist/app/server/sources/jobs.js +59 -0
  30. package/dist/app/server/sources/processes.js +99 -0
  31. package/dist/app/server/sources/projects.js +165 -0
  32. package/dist/app/server/sources/spawns.js +297 -0
  33. package/dist/app/server/sources/subagents.js +62 -0
  34. package/dist/app/server/sources/tasks.js +30 -0
  35. package/dist/app/server/sources/transcripts.js +121 -0
  36. package/dist/app/server/util/jsonl.js +124 -0
  37. package/dist/app/server/util/sanitize.js +42 -0
  38. package/dist/app/server/util/semver.js +33 -0
  39. package/dist/app/shared/pricing.js +57 -0
  40. package/dist/app/shared/tools.js +62 -0
  41. package/dist/app/shared/types.js +3 -0
  42. package/dist/web/assets/index-DdxYQpDc.js +14 -0
  43. package/dist/web/assets/index-VdEOo0XQ.css +1 -0
  44. package/dist/web/index.html +22 -0
  45. package/package.json +69 -0
@@ -0,0 +1,87 @@
1
+ import { compareVersions } from '../util/semver.js';
2
+ import { isObj, str } from './util.js';
3
+ export class FormatRegistry {
4
+ decoders = new Map();
5
+ hits = new Map();
6
+ drift = new Map();
7
+ register(d) {
8
+ const list = this.decoders.get(d.source) ?? [];
9
+ if (list.some((x) => x.id === d.id))
10
+ throw new Error(`Decoder ${d.id} doppelt registriert`);
11
+ list.push(d);
12
+ this.decoders.set(d.source, list);
13
+ return this;
14
+ }
15
+ /** Wählt den am besten passenden Decoder und dekodiert den Record. */
16
+ decode(source, raw, file, line = 0, opts = {}) {
17
+ const record = isObj(raw) ? raw : { __nonObject: raw };
18
+ const version = str(record.version) ?? str(record.cliVersion);
19
+ const ctx = {
20
+ source,
21
+ file,
22
+ line,
23
+ reportUnknown: (recordType, o, known) => {
24
+ if (!o)
25
+ return undefined;
26
+ let extra;
27
+ for (const k of Object.keys(o)) {
28
+ if (known.includes(k))
29
+ continue;
30
+ (extra ??= {})[k] = o[k];
31
+ if (!opts.silent)
32
+ this.noteDrift(source, recordType, k, version);
33
+ }
34
+ return extra;
35
+ },
36
+ };
37
+ let best;
38
+ let bestScore = 0;
39
+ for (const d of this.decoders.get(source) ?? []) {
40
+ const s = d.match(record, ctx);
41
+ if (s > bestScore) {
42
+ best = d;
43
+ bestScore = s;
44
+ }
45
+ }
46
+ if (!best)
47
+ throw new Error(`No decoder (not even a fallback) for source ${source}`);
48
+ if (opts.silent)
49
+ return { out: best.decode(record, ctx), decoder: best.id };
50
+ this.hits.set(best.id, (this.hits.get(best.id) ?? 0) + 1);
51
+ if (bestScore <= 1)
52
+ this.noteDrift(source, str(record.type) ?? '-', '(unknown record type)', version);
53
+ const known = best.knownFields?.(record);
54
+ if (known) {
55
+ const recordType = str(record.type) ?? '-';
56
+ for (const k of Object.keys(record))
57
+ if (!known.includes(k))
58
+ this.noteDrift(source, recordType, k, version);
59
+ }
60
+ return { out: best.decode(record, ctx), decoder: best.id };
61
+ }
62
+ noteDrift(source, recordType, field, version) {
63
+ const key = `${source}\u0000${recordType}\u0000${field}`;
64
+ let d = this.drift.get(key);
65
+ if (!d) {
66
+ d = { source, recordType, field, count: 0 };
67
+ this.drift.set(key, d);
68
+ }
69
+ d.count++;
70
+ if (version) {
71
+ if (!d.firstVersion || compareVersions(version, d.firstVersion) < 0)
72
+ d.firstVersion = version;
73
+ if (!d.lastVersion || compareVersions(version, d.lastVersion) > 0)
74
+ d.lastVersion = version;
75
+ }
76
+ }
77
+ report() {
78
+ const decoders = [];
79
+ for (const list of this.decoders.values()) {
80
+ for (const d of list) {
81
+ decoders.push({ id: d.id, source: d.source, description: d.description, versions: d.versions, hits: this.hits.get(d.id) ?? 0 });
82
+ }
83
+ }
84
+ const drift = [...this.drift.values()].sort((a, b) => b.count - a.count).map((d) => ({ ...d }));
85
+ return { decoders, drift };
86
+ }
87
+ }
@@ -0,0 +1,28 @@
1
+ import { sanitize } from '../../util/sanitize.js';
2
+ import { extraFields, num, str } from '../util.js';
3
+ const FIELDS = ['agentType', 'description', 'toolUseId', 'spawnDepth', 'requestShape', 'requestNonInteractive'];
4
+ export const subagentMetaDecoder = {
5
+ id: 'subagent-meta@1',
6
+ source: 'subagent-meta',
7
+ description: 'subagent metadata: type, description, calling tool use, depth',
8
+ match: (raw) => ('agentType' in raw || 'toolUseId' in raw ? 10 : 0),
9
+ decode(raw) {
10
+ const extra = extraFields(raw, FIELDS);
11
+ return {
12
+ agentType: str(raw.agentType),
13
+ description: str(raw.description),
14
+ toolUseId: str(raw.toolUseId),
15
+ spawnDepth: num(raw.spawnDepth),
16
+ background: raw.requestShape === 'background',
17
+ extra: extra ? sanitize(extra) : undefined,
18
+ };
19
+ },
20
+ knownFields: () => FIELDS,
21
+ };
22
+ export const subagentMetaFallback = {
23
+ id: 'subagent-meta.fallback',
24
+ source: 'subagent-meta',
25
+ description: 'unknown subagent metadata',
26
+ match: () => 1,
27
+ decode: (raw) => ({ extra: sanitize(raw) }),
28
+ };
@@ -0,0 +1,33 @@
1
+ import { sanitize } from '../../util/sanitize.js';
2
+ import { arr, extraFields, str } from '../util.js';
3
+ const FIELDS = ['id', 'subject', 'description', 'status', 'activeForm', 'owner', 'blocks', 'blockedBy'];
4
+ const ids = (v) => arr(v).map(String);
5
+ /** tasks/<sessionId>/<n>.json (TaskCreate/TaskUpdate). */
6
+ export const taskDecoder = {
7
+ id: 'task@1',
8
+ source: 'task',
9
+ description: 'task of a session task list (status, dependencies)',
10
+ match: (raw) => ('subject' in raw || 'status' in raw) && 'id' in raw ? 10 : 0,
11
+ decode(raw) {
12
+ const extra = extraFields(raw, FIELDS);
13
+ return {
14
+ id: String(raw.id),
15
+ subject: str(raw.subject),
16
+ description: str(raw.description),
17
+ status: str(raw.status),
18
+ activeForm: str(raw.activeForm),
19
+ owner: str(raw.owner),
20
+ blocks: ids(raw.blocks),
21
+ blockedBy: ids(raw.blockedBy),
22
+ extra: extra ? sanitize(extra) : undefined,
23
+ };
24
+ },
25
+ knownFields: () => FIELDS,
26
+ };
27
+ export const taskFallback = {
28
+ id: 'task.fallback',
29
+ source: 'task',
30
+ description: 'unknown task format',
31
+ match: () => 1,
32
+ decode: (raw) => ({ id: String(raw.id ?? '?'), blocks: [], blockedBy: [], extra: sanitize(raw) }),
33
+ };
@@ -0,0 +1,33 @@
1
+ import { bool, str } from '../util.js';
2
+ /** Felder, die (fast) jeder Nachrichten-Record trägt. */
3
+ export const ENVELOPE_FIELDS = [
4
+ 'type',
5
+ 'uuid',
6
+ 'parentUuid',
7
+ 'logicalParentUuid',
8
+ 'timestamp',
9
+ 'isSidechain',
10
+ 'isMeta',
11
+ 'userType',
12
+ 'cwd',
13
+ 'sessionId',
14
+ 'session_id',
15
+ 'sessionKind',
16
+ 'version',
17
+ 'gitBranch',
18
+ 'entrypoint',
19
+ 'slug',
20
+ 'agentId',
21
+ ];
22
+ export function envelope(raw) {
23
+ return {
24
+ recordType: str(raw.type) ?? '-',
25
+ version: str(raw.version),
26
+ uuid: str(raw.uuid),
27
+ parentUuid: raw.parentUuid === null ? null : str(raw.parentUuid),
28
+ timestamp: str(raw.timestamp),
29
+ agentId: str(raw.agentId),
30
+ isSidechain: bool(raw.isSidechain),
31
+ isMeta: bool(raw.isMeta),
32
+ };
33
+ }
@@ -0,0 +1,206 @@
1
+ import { clip, sanitize } from '../../util/sanitize.js';
2
+ import { inRange } from '../../util/semver.js';
3
+ import { arr, bool, extraFields, isObj, obj, str } from '../util.js';
4
+ import { ENVELOPE_FIELDS, envelope } from './common.js';
5
+ /**
6
+ * user- und assistant-Records im Messages-API-Format:
7
+ * { type: "user"|"assistant", message: { role, content: string | Block[] , … }, … }
8
+ * Belegt für 2.1.72 – 2.1.283. `message.content` ist bei Nutzer-Prompts teils
9
+ * ein String, teils ein Block-Array; `toolUseResult` ist teils Objekt, teils String.
10
+ */
11
+ const USER_FIELDS = [
12
+ ...ENVELOPE_FIELDS,
13
+ 'message',
14
+ 'promptId',
15
+ 'sourceToolAssistantUUID',
16
+ 'sourceToolUseID',
17
+ 'toolUseResult',
18
+ 'permissionMode',
19
+ 'promptSource',
20
+ 'origin',
21
+ 'toolDenialKind',
22
+ 'isCompactSummary',
23
+ 'isVisibleInTranscriptOnly',
24
+ 'interruptedMessageId',
25
+ 'turnOrigin',
26
+ 'turnCompanion',
27
+ ];
28
+ const ASSISTANT_FIELDS = [
29
+ ...ENVELOPE_FIELDS,
30
+ 'message',
31
+ 'requestId',
32
+ 'effort',
33
+ 'perTurnEffort',
34
+ 'apiBlockIndex',
35
+ 'attributionAgent',
36
+ 'attributionSkill',
37
+ 'attributionMcpServer',
38
+ 'attributionMcpTool',
39
+ 'attributionPlugin',
40
+ 'advisorModel',
41
+ 'isApiErrorMessage',
42
+ 'error',
43
+ 'apiErrorStatus',
44
+ ];
45
+ const ASSISTANT_MESSAGE_FIELDS = [
46
+ 'model',
47
+ 'id',
48
+ 'type',
49
+ 'role',
50
+ 'content',
51
+ 'stop_reason',
52
+ 'stop_sequence',
53
+ 'stop_details',
54
+ 'usage',
55
+ 'diagnostics',
56
+ 'context_management',
57
+ 'container',
58
+ 'input_transformations',
59
+ ];
60
+ /** Vollständige Dateiinhalte in toolUseResult (Edit/Write) – für die Anzeige unnötig. */
61
+ const DROP_IN_RESULT = new Set(['originalFile']);
62
+ function resultParts(content) {
63
+ if (typeof content === 'string')
64
+ return [{ type: 'text', ...clip(content) }];
65
+ return arr(content).map((b) => {
66
+ if (!isObj(b))
67
+ return { type: 'other', value: b };
68
+ switch (b.type) {
69
+ case 'text':
70
+ return { type: 'text', ...clip(str(b.text) ?? '') };
71
+ case 'image':
72
+ return { type: 'image', mediaType: str(obj(b.source)?.media_type) };
73
+ case 'tool_reference':
74
+ return { type: 'tool_reference', toolName: str(b.tool_name) ?? '?' };
75
+ default:
76
+ return { type: 'other', value: sanitize(b) };
77
+ }
78
+ });
79
+ }
80
+ function decodeUser(raw, ctx) {
81
+ const env = envelope(raw);
82
+ const msg = obj(raw.message) ?? {};
83
+ const content = msg.content;
84
+ const out = [];
85
+ const userFields = {
86
+ permissionMode: str(raw.permissionMode),
87
+ promptSource: str(raw.promptSource),
88
+ origin: raw.origin,
89
+ isCompactSummary: bool(raw.isCompactSummary),
90
+ };
91
+ if (typeof content === 'string') {
92
+ out.push({ ...env, kind: 'user-text', text: clip(content).text, ...userFields });
93
+ }
94
+ else {
95
+ for (const b of arr(content)) {
96
+ if (!isObj(b))
97
+ continue;
98
+ switch (b.type) {
99
+ case 'text':
100
+ out.push({ ...env, kind: 'user-text', text: clip(str(b.text) ?? '').text, ...userFields });
101
+ break;
102
+ case 'tool_result': {
103
+ const tur = raw.toolUseResult;
104
+ out.push({
105
+ ...env,
106
+ kind: 'tool-result',
107
+ toolUseId: str(b.tool_use_id) ?? '',
108
+ parts: resultParts(b.content),
109
+ isError: b.is_error === true,
110
+ structured: tur === undefined ? undefined : sanitize(tur, { dropKeys: DROP_IN_RESULT }),
111
+ denied: str(raw.toolDenialKind),
112
+ });
113
+ break;
114
+ }
115
+ case 'image':
116
+ out.push({ ...env, kind: 'image', mediaType: str(obj(b.source)?.media_type) });
117
+ break;
118
+ default:
119
+ out.push({ ...env, kind: 'unknown', raw: sanitize(b) });
120
+ ctx.reportUnknown('user.message.content', { [String(b.type)]: true }, []);
121
+ }
122
+ }
123
+ }
124
+ if (out.length === 0)
125
+ out.push({ ...env, kind: 'user-text', text: '', ...userFields });
126
+ const extra = extraFields(raw, USER_FIELDS);
127
+ if (extra)
128
+ out[0].extra = sanitize(extra);
129
+ return out;
130
+ }
131
+ function decodeAssistant(raw, ctx) {
132
+ const env = envelope(raw);
133
+ const msg = obj(raw.message) ?? {};
134
+ const msgExtra = ctx.reportUnknown('assistant.message', msg, ASSISTANT_MESSAGE_FIELDS);
135
+ const fields = {
136
+ model: str(msg.model),
137
+ messageId: str(msg.id),
138
+ requestId: str(raw.requestId),
139
+ usage: obj(msg.usage),
140
+ stopReason: msg.stop_reason === null ? null : str(msg.stop_reason),
141
+ effort: str(raw.perTurnEffort) ?? str(raw.effort),
142
+ attribution: {
143
+ skill: str(raw.attributionSkill),
144
+ agent: str(raw.attributionAgent),
145
+ mcpServer: str(raw.attributionMcpServer),
146
+ mcpTool: str(raw.attributionMcpTool),
147
+ plugin: str(raw.attributionPlugin),
148
+ },
149
+ isApiError: bool(raw.isApiErrorMessage),
150
+ };
151
+ const out = [];
152
+ for (const b of arr(msg.content)) {
153
+ if (!isObj(b))
154
+ continue;
155
+ switch (b.type) {
156
+ case 'text':
157
+ out.push({ ...env, ...fields, kind: 'assistant-text', text: clip(str(b.text) ?? '').text });
158
+ break;
159
+ case 'thinking':
160
+ out.push({ ...env, ...fields, kind: 'thinking', text: clip(str(b.thinking) ?? '').text });
161
+ break;
162
+ case 'redacted_thinking':
163
+ out.push({ ...env, ...fields, kind: 'thinking', text: '', redacted: true });
164
+ break;
165
+ case 'tool_use':
166
+ case 'server_tool_use':
167
+ out.push({
168
+ ...env,
169
+ ...fields,
170
+ kind: 'tool-use',
171
+ toolUseId: str(b.id) ?? '',
172
+ name: str(b.name) ?? '?',
173
+ input: sanitize(obj(b.input) ?? {}) ?? {},
174
+ });
175
+ break;
176
+ default:
177
+ out.push({ ...env, kind: 'unknown', raw: sanitize(b) });
178
+ ctx.reportUnknown('assistant.message.content', { [String(b.type)]: true }, []);
179
+ }
180
+ }
181
+ if (out.length === 0) {
182
+ // z. B. API-Fehlermeldung ohne Inhalt
183
+ out.push({ ...env, ...fields, kind: 'assistant-text', text: str(raw.error) ?? '' });
184
+ }
185
+ const extra = { ...extraFields(raw, ASSISTANT_FIELDS), ...(msgExtra ? { message: msgExtra } : {}) };
186
+ if (Object.keys(extra).length)
187
+ out[0].extra = sanitize(extra);
188
+ return out;
189
+ }
190
+ export const messageDecoder = {
191
+ id: 'transcript.message@2',
192
+ source: 'transcript',
193
+ description: 'user/assistant records with Messages API content (text, thinking, tool calls and results)',
194
+ versions: '>=2.0.0',
195
+ match(raw) {
196
+ if ((raw.type !== 'user' && raw.type !== 'assistant') || !isObj(raw.message))
197
+ return 0;
198
+ return 10 + (inRange(str(raw.version), '>=2.0.0') ? 5 : 0);
199
+ },
200
+ decode(raw, ctx) {
201
+ return raw.type === 'assistant' ? decodeAssistant(raw, ctx) : decodeUser(raw, ctx);
202
+ },
203
+ knownFields(raw) {
204
+ return raw.type === 'assistant' ? ASSISTANT_FIELDS : USER_FIELDS;
205
+ },
206
+ };
@@ -0,0 +1,130 @@
1
+ import { sanitize } from '../../util/sanitize.js';
2
+ import { extraFields, isObj, obj, str, without } from '../util.js';
3
+ import { ENVELOPE_FIELDS, envelope } from './common.js';
4
+ // ---------------------------------------------------------------------------
5
+ // system-Records: stop_hook_summary, turn_duration, api_error, compact_boundary, …
6
+ const SYSTEM_FIELDS = [
7
+ ...ENVELOPE_FIELDS,
8
+ 'subtype',
9
+ 'level',
10
+ 'content',
11
+ // subtype-spezifisch
12
+ 'hookCount',
13
+ 'hookInfos',
14
+ 'hookErrors',
15
+ 'hookAdditionalContext',
16
+ 'preventedContinuation',
17
+ 'stopReason',
18
+ 'hasOutput',
19
+ 'toolUseID',
20
+ 'durationMs',
21
+ 'messageCount',
22
+ 'error',
23
+ 'retryInMs',
24
+ 'retryAttempt',
25
+ 'maxRetries',
26
+ 'source',
27
+ 'pendingBackgroundAgentCount',
28
+ 'compactMetadata',
29
+ 'commandRun',
30
+ 'usageReport',
31
+ 'url',
32
+ 'contextUsage',
33
+ ];
34
+ export const systemDecoder = {
35
+ id: 'transcript.system@2',
36
+ source: 'transcript',
37
+ description: 'system records (hooks, turn duration, API errors, compaction, Remote Control …)',
38
+ versions: '>=2.0.0',
39
+ match: (raw) => (raw.type === 'system' && typeof raw.subtype === 'string' ? 10 : 0),
40
+ decode(raw) {
41
+ const data = sanitize(without(raw, [...ENVELOPE_FIELDS, 'subtype', 'level', 'content']));
42
+ return [
43
+ {
44
+ ...envelope(raw),
45
+ kind: 'system',
46
+ subtype: str(raw.subtype) ?? '?',
47
+ level: str(raw.level),
48
+ text: str(raw.content),
49
+ data,
50
+ },
51
+ ];
52
+ },
53
+ knownFields: () => SYSTEM_FIELDS,
54
+ };
55
+ // ---------------------------------------------------------------------------
56
+ // attachment-Records: Kontext, den Claude Code dem Modell mitgibt
57
+ const ATTACHMENT_FIELDS = [...ENVELOPE_FIELDS, 'attachment', 'rendered', 'renderedInHumanTurn'];
58
+ export const attachmentDecoder = {
59
+ id: 'transcript.attachment@2',
60
+ source: 'transcript',
61
+ description: 'attachment records (files, reminders, skill/tool listings, hook context …)',
62
+ versions: '>=2.1.0',
63
+ match: (raw) => (raw.type === 'attachment' && isObj(raw.attachment) ? 10 : 0),
64
+ decode(raw) {
65
+ const att = obj(raw.attachment) ?? {};
66
+ const entry = {
67
+ ...envelope(raw),
68
+ kind: 'attachment',
69
+ attachmentType: str(att.type) ?? '?',
70
+ data: sanitize(without(att, ['type'])),
71
+ };
72
+ const extra = extraFields(raw, ATTACHMENT_FIELDS);
73
+ if (raw.rendered !== undefined)
74
+ entry.data.__rendered = sanitize(raw.rendered);
75
+ if (extra)
76
+ entry.extra = sanitize(extra);
77
+ return [entry];
78
+ },
79
+ knownFields: () => ATTACHMENT_FIELDS,
80
+ };
81
+ // ---------------------------------------------------------------------------
82
+ // Zustands-Records ohne Nachrichteninhalt: Titel, Modus, Kosten, Queue …
83
+ export const META_TYPES = [
84
+ 'custom-title',
85
+ 'ai-title',
86
+ 'agent-name',
87
+ 'agent-setting',
88
+ 'mode',
89
+ 'permission-mode',
90
+ 'last-prompt',
91
+ 'cost-state',
92
+ 'worktree-state',
93
+ 'relocated',
94
+ 'continued-in',
95
+ 'bridge-session',
96
+ 'queue-operation',
97
+ 'file-history-snapshot',
98
+ 'file-history-delta',
99
+ 'frame-link',
100
+ 'atis-latch',
101
+ 'artifact-comment-monitor',
102
+ 'artifact-autoreact-ledger',
103
+ ];
104
+ export const metaDecoder = {
105
+ id: 'transcript.meta@2',
106
+ source: 'transcript',
107
+ description: `state records (${META_TYPES.length} types: titles, mode, cost, queue, worktree …)`,
108
+ versions: '>=2.1.0',
109
+ match: (raw) => (typeof raw.type === 'string' && META_TYPES.includes(raw.type) ? 10 : 0),
110
+ decode(raw) {
111
+ return [
112
+ {
113
+ ...envelope(raw),
114
+ kind: 'meta',
115
+ metaType: str(raw.type),
116
+ data: sanitize(without(raw, ['type', 'sessionId', 'timestamp'])),
117
+ },
118
+ ];
119
+ },
120
+ };
121
+ // ---------------------------------------------------------------------------
122
+ export const transcriptFallback = {
123
+ id: 'transcript.fallback',
124
+ source: 'transcript',
125
+ description: 'unknown records – shown raw and reported as schema drift',
126
+ match: () => 1,
127
+ decode(raw) {
128
+ return [{ ...envelope(raw), kind: 'unknown', raw: sanitize(raw) }];
129
+ },
130
+ };
@@ -0,0 +1,10 @@
1
+ /**
2
+ * Format-Erkennung: Jede Datenquelle (SourceKind) hat eine Reihe von Decodern.
3
+ * Für jeden Record fragt die Registry alle Decoder der Quelle per match() nach
4
+ * einem Score und nimmt den höchsten. Jede Quelle hat einen Fallback-Decoder,
5
+ * damit nie ein Record verloren geht.
6
+ *
7
+ * Neues Format hinzufügen: neue Datei mit einem Decoder anlegen und in
8
+ * formats/index.ts registrieren. Details: docs/FORMATS.md
9
+ */
10
+ export {};
@@ -0,0 +1,22 @@
1
+ export const isObj = (v) => !!v && typeof v === 'object' && !Array.isArray(v);
2
+ export const str = (v) => (typeof v === 'string' ? v : undefined);
3
+ export const num = (v) => (typeof v === 'number' && Number.isFinite(v) ? v : undefined);
4
+ export const bool = (v) => (typeof v === 'boolean' ? v : undefined);
5
+ export const obj = (v) => (isObj(v) ? v : undefined);
6
+ export const arr = (v) => (Array.isArray(v) ? v : []);
7
+ /** Alle Felder von `raw`, die nicht in `known` stehen – oder undefined, wenn keine. */
8
+ export function extraFields(raw, known) {
9
+ let out;
10
+ for (const k of Object.keys(raw)) {
11
+ if (!known.includes(k))
12
+ (out ??= {})[k] = raw[k];
13
+ }
14
+ return out;
15
+ }
16
+ export function without(raw, keys) {
17
+ const out = {};
18
+ for (const [k, v] of Object.entries(raw))
19
+ if (!keys.includes(k))
20
+ out[k] = v;
21
+ return out;
22
+ }