@unrdf/kgc-probe 26.4.2

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,355 @@
1
+ /**
2
+ * @fileoverview Markdown reporter for KGC Probe observations
3
+ *
4
+ * Generates human-readable reports from observations with:
5
+ * - Executive summary (platform, runtime, key capabilities)
6
+ * - Capabilities by domain (grouped)
7
+ * - Constraints by domain (grouped)
8
+ * - Performance metrics (tables)
9
+ * - Guard denials (if any)
10
+ * - Provenance (observation count, hash chain)
11
+ *
12
+ * Design principles:
13
+ * - Human-readable: Clear structure, visual hierarchy
14
+ * - Comprehensive: All relevant information included
15
+ * - Actionable: Highlights issues and limitations
16
+ */
17
+
18
+ import crypto from 'crypto';
19
+ import { deriveCapabilities, deriveConstraints } from './rdf.mjs';
20
+
21
+ /**
22
+ * Generate deterministic hash for observation
23
+ *
24
+ * @param {Object} observation - Observation object
25
+ * @returns {string} SHA-256 hash (first 16 chars)
26
+ */
27
+ function generateHash(observation) {
28
+ if (observation.hash) return observation.hash;
29
+ if (observation.receiptHash) return observation.receiptHash;
30
+
31
+ const content = JSON.stringify({
32
+ method: observation.method,
33
+ category: observation.category,
34
+ message: observation.message,
35
+ outputs: observation.outputs || observation.data,
36
+ timestamp: observation.timestamp || observation.metadata?.timestamp,
37
+ });
38
+
39
+ return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
40
+ }
41
+
42
+ /**
43
+ * Extract platform information from observations
44
+ *
45
+ * @param {Array<Object>} observations - Array of observations
46
+ * @returns {Object} Platform info
47
+ */
48
+ function extractPlatformInfo(observations) {
49
+ const info = {
50
+ platform: 'unknown',
51
+ runtime: 'unknown',
52
+ arch: 'unknown',
53
+ nodeVersion: 'unknown'
54
+ };
55
+
56
+ for (const obs of observations) {
57
+ const data = obs.outputs || obs.data || {};
58
+
59
+ // Extract Node.js version
60
+ if (data.nodeVersion) {
61
+ info.nodeVersion = data.nodeVersion;
62
+ info.runtime = `Node.js ${data.nodeVersion}`;
63
+ }
64
+
65
+ // Extract platform/arch
66
+ if (data.platform) info.platform = data.platform;
67
+ if (data.arch) info.arch = data.arch;
68
+ }
69
+
70
+ return info;
71
+ }
72
+
73
+ /**
74
+ * Group observations by domain
75
+ *
76
+ * @param {Array<Object>} observations - Array of observations
77
+ * @returns {Map<string, Array<Object>>} Observations grouped by domain
78
+ */
79
+ function groupByDomain(observations) {
80
+ const byDomain = new Map();
81
+
82
+ for (const obs of observations) {
83
+ const domain = obs.domain || obs.category || 'general';
84
+ if (!byDomain.has(domain)) {
85
+ byDomain.set(domain, []);
86
+ }
87
+ byDomain.get(domain).push(obs);
88
+ }
89
+
90
+ return byDomain;
91
+ }
92
+
93
+ /**
94
+ * Extract performance metrics from observations
95
+ *
96
+ * @param {Array<Object>} observations - Array of observations
97
+ * @returns {Array<Object>} Performance metrics
98
+ */
99
+ function extractPerformanceMetrics(observations) {
100
+ const metrics = [];
101
+
102
+ for (const obs of observations) {
103
+ const data = obs.outputs || obs.data || {};
104
+
105
+ // Check for performance data
106
+ if (data.mean !== undefined || data.median !== undefined || data.p95 !== undefined) {
107
+ metrics.push({
108
+ method: obs.method || obs.message || 'unknown',
109
+ mean: data.mean,
110
+ median: data.median,
111
+ p95: data.p95,
112
+ min: data.min,
113
+ max: data.max,
114
+ unit: data.unit || 'ms',
115
+ samples: data.samples || 1
116
+ });
117
+ }
118
+
119
+ // Check for throughput data
120
+ if (data.throughputMBps !== undefined) {
121
+ metrics.push({
122
+ method: obs.method || obs.message || 'unknown',
123
+ throughput: data.throughputMBps,
124
+ unit: 'MB/s',
125
+ samples: data.samples || 1
126
+ });
127
+ }
128
+ }
129
+
130
+ return metrics;
131
+ }
132
+
133
+ /**
134
+ * Extract guard denials from observations
135
+ *
136
+ * @param {Array<Object>} observations - Array of observations
137
+ * @returns {Array<Object>} Guard denials
138
+ */
139
+ function extractGuardDenials(observations) {
140
+ const denials = [];
141
+
142
+ for (const obs of observations) {
143
+ const data = obs.outputs || obs.data || {};
144
+
145
+ if (obs.guardDecision === 'denied' || obs.category === 'guard' || data.guardDecision === 'denied') {
146
+ denials.push({
147
+ method: obs.method || obs.message || 'unknown',
148
+ reason: data.reason || obs.message || 'Access denied',
149
+ guard: data.guardName || 'unknown',
150
+ hash: generateHash(obs)
151
+ });
152
+ }
153
+ }
154
+
155
+ return denials;
156
+ }
157
+
158
+ /**
159
+ * Calculate hash chain from observations
160
+ *
161
+ * @param {Array<Object>} observations - Array of observations
162
+ * @returns {string} Chain hash
163
+ */
164
+ function calculateChainHash(observations) {
165
+ const hashes = observations.map(obs => generateHash(obs)).sort();
166
+ const content = hashes.join('');
167
+ return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
168
+ }
169
+
170
+ /**
171
+ * Render report as Markdown
172
+ *
173
+ * @param {Array<Object>} observations - Array of observation objects
174
+ * @returns {string} Markdown-formatted report
175
+ *
176
+ * @example
177
+ * const report = renderReport(observations);
178
+ * console.log(report); // # KGC Probe Report\n\n## Executive Summary\n...
179
+ */
180
+ export function renderReport(observations) {
181
+ // Sort observations for deterministic output
182
+ const sortedObs = [...observations].sort((a, b) => {
183
+ const hashA = generateHash(a);
184
+ const hashB = generateHash(b);
185
+ return hashA.localeCompare(hashB);
186
+ });
187
+
188
+ // Extract components
189
+ const platform = extractPlatformInfo(sortedObs);
190
+ const capabilities = deriveCapabilities(sortedObs);
191
+ const constraints = deriveConstraints(sortedObs);
192
+ const byDomain = groupByDomain(sortedObs);
193
+ const perfMetrics = extractPerformanceMetrics(sortedObs);
194
+ const guardDenials = extractGuardDenials(sortedObs);
195
+ const chainHash = calculateChainHash(sortedObs);
196
+
197
+ // Calculate run ID from first timestamp
198
+ const firstTimestamp = sortedObs.length > 0
199
+ ? (sortedObs[0].timestamp || sortedObs[0].metadata?.timestamp || Date.now())
200
+ : Date.now();
201
+ const runDate = typeof firstTimestamp === 'number'
202
+ ? new Date(firstTimestamp).toISOString()
203
+ : firstTimestamp;
204
+ const runId = `run_${runDate.replace(/[:.]/g, '-').substring(0, 23)}`;
205
+
206
+ // Build Markdown report
207
+ let report = '# KGC Probe Report\n\n';
208
+
209
+ // Metadata header
210
+ report += `**Run ID**: ${runId} \n`;
211
+ report += `**Observations**: ${sortedObs.length} \n`;
212
+ report += `**Hash**: sha256:${chainHash}\n\n`;
213
+
214
+ // Executive Summary
215
+ report += '## Executive Summary\n\n';
216
+ report += `- **Platform**: ${platform.platform} ${platform.arch}\n`;
217
+ report += `- **Runtime**: ${platform.runtime}\n`;
218
+
219
+ // Key capabilities summary
220
+ const wasmCap = capabilities.find(c => c.name.includes('wasm'));
221
+ const workerCap = capabilities.find(c => c.name.includes('worker'));
222
+
223
+ report += `- **WASM**: ${wasmCap ? 'Available' : 'Not detected'}\n`;
224
+ report += `- **Workers**: ${workerCap ? 'Available' : 'Not detected'}\n`;
225
+ report += `- **Capabilities**: ${capabilities.length} discovered\n`;
226
+ report += `- **Constraints**: ${constraints.length} detected\n`;
227
+ report += '\n';
228
+
229
+ // Capabilities Section
230
+ report += '## Capabilities\n\n';
231
+
232
+ if (capabilities.length > 0) {
233
+ // Group capabilities by domain
234
+ const capByDomain = new Map();
235
+ for (const cap of capabilities) {
236
+ const domain = cap.name.split('.')[0] || 'general';
237
+ if (!capByDomain.has(domain)) {
238
+ capByDomain.set(domain, []);
239
+ }
240
+ capByDomain.get(domain).push(cap);
241
+ }
242
+
243
+ // Sort domains alphabetically
244
+ const sortedDomains = Array.from(capByDomain.keys()).sort();
245
+
246
+ for (const domain of sortedDomains) {
247
+ report += `### ${domain.charAt(0).toUpperCase() + domain.slice(1)}\n\n`;
248
+
249
+ const domainCaps = capByDomain.get(domain);
250
+ for (const cap of domainCaps) {
251
+ report += `- ✅ **${cap.name}**\n`;
252
+
253
+ if (cap.data && Object.keys(cap.data).length > 0) {
254
+ const dataStr = JSON.stringify(cap.data, null, 2);
255
+ report += ` \`\`\`json\n ${dataStr.split('\n').join('\n ')}\n \`\`\`\n`;
256
+ }
257
+ }
258
+ report += '\n';
259
+ }
260
+ } else {
261
+ report += '*No capabilities detected*\n\n';
262
+ }
263
+
264
+ // Constraints Section
265
+ report += '## Constraints\n\n';
266
+
267
+ if (constraints.length > 0) {
268
+ // Group constraints by type
269
+ const constraintByType = new Map();
270
+ for (const constraint of constraints) {
271
+ const type = constraint.type || 'general';
272
+ if (!constraintByType.has(type)) {
273
+ constraintByType.set(type, []);
274
+ }
275
+ constraintByType.get(type).push(constraint);
276
+ }
277
+
278
+ // Sort types alphabetically
279
+ const sortedTypes = Array.from(constraintByType.keys()).sort();
280
+
281
+ for (const type of sortedTypes) {
282
+ report += `### ${type.split('-').map(w => w.charAt(0).toUpperCase() + w.slice(1)).join(' ')}\n\n`;
283
+
284
+ const typeConstraints = constraintByType.get(type);
285
+ for (const constraint of typeConstraints) {
286
+ report += `- ⚠️ ${constraint.description}\n`;
287
+ }
288
+ report += '\n';
289
+ }
290
+ } else {
291
+ report += '*No constraints detected*\n\n';
292
+ }
293
+
294
+ // Performance Metrics Section
295
+ if (perfMetrics.length > 0) {
296
+ report += '## Performance\n\n';
297
+ report += '| Method | Mean | Median | P95 | Min | Max | Unit | Samples |\n';
298
+ report += '|--------|------|--------|-----|-----|-----|------|--------|\n';
299
+
300
+ for (const metric of perfMetrics) {
301
+ const mean = metric.mean !== undefined ? metric.mean.toFixed(2) : '-';
302
+ const median = metric.median !== undefined ? metric.median.toFixed(2) : '-';
303
+ const p95 = metric.p95 !== undefined ? metric.p95.toFixed(2) : '-';
304
+ const min = metric.min !== undefined ? metric.min.toFixed(2) : '-';
305
+ const max = metric.max !== undefined ? metric.max.toFixed(2) : '-';
306
+ const throughput = metric.throughput !== undefined ? metric.throughput.toFixed(2) : '-';
307
+
308
+ if (metric.throughput !== undefined) {
309
+ report += `| ${metric.method} | ${throughput} | - | - | - | - | ${metric.unit} | ${metric.samples} |\n`;
310
+ } else {
311
+ report += `| ${metric.method} | ${mean} | ${median} | ${p95} | ${min} | ${max} | ${metric.unit} | ${metric.samples} |\n`;
312
+ }
313
+ }
314
+ report += '\n';
315
+ }
316
+
317
+ // Guard Denials Section
318
+ if (guardDenials.length > 0) {
319
+ report += '## Guard Denials\n\n';
320
+ report += `${guardDenials.length} operation(s) were denied by security guards:\n\n`;
321
+
322
+ for (const denial of guardDenials) {
323
+ report += `- **${denial.method}**\n`;
324
+ report += ` - Guard: \`${denial.guard}\`\n`;
325
+ report += ` - Reason: ${denial.reason}\n`;
326
+ report += ` - Hash: \`${denial.hash}\`\n\n`;
327
+ }
328
+ }
329
+
330
+ // Provenance Section
331
+ report += '## Provenance\n\n';
332
+ report += `- **Observation count**: ${sortedObs.length}\n`;
333
+ report += `- **Hash chain**: \`sha256:${chainHash}\`\n`;
334
+ report += `- **Domains probed**: ${byDomain.size}\n`;
335
+
336
+ const domainCounts = Array.from(byDomain.entries())
337
+ .map(([domain, obs]) => ` - ${domain}: ${obs.length}`)
338
+ .join('\n');
339
+
340
+ report += '\n**Observations by domain**:\n';
341
+ report += domainCounts + '\n\n';
342
+
343
+ // Footer
344
+ report += `---\n\n`;
345
+ report += `*Report generated at ${new Date().toISOString()}*\n`;
346
+
347
+ return report;
348
+ }
349
+
350
+ export default {
351
+ renderReport,
352
+ extractPlatformInfo,
353
+ extractPerformanceMetrics,
354
+ extractGuardDenials
355
+ };