@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,555 @@
1
+ /**
2
+ * KGC Probe Reporter - Convert observations to RDF/Turtle and generate reports
3
+ *
4
+ * Transforms runtime observations into semantic RDF graphs and human-readable
5
+ * Markdown reports with derived capabilities and constraints.
6
+ */
7
+
8
+ import { createStore, dataFactory } from '@unrdf/oxigraph';
9
+ import { z } from 'zod';
10
+ import crypto from 'crypto';
11
+
12
+ // ===== Zod Schemas =====
13
+
14
+ /**
15
+ * Schema for probe observation metadata
16
+ */
17
+ const ObservationSchema = z.object({
18
+ method: z.string(),
19
+ domain: z.string().optional(),
20
+ timestamp: z.number().optional(),
21
+ outputs: z.any(),
22
+ error: z.string().optional(),
23
+ guardDecision: z.string().optional(),
24
+ hash: z.string().optional(),
25
+ });
26
+
27
+ /**
28
+ * Schema for derived capability claim
29
+ */
30
+ const CapabilitySchema = z.object({
31
+ type: z.literal('capability'),
32
+ title: z.string(),
33
+ description: z.string(),
34
+ evidence: z.array(z.string()),
35
+ });
36
+
37
+ /**
38
+ * Schema for derived constraint claim
39
+ */
40
+ const ConstraintSchema = z.object({
41
+ type: z.literal('constraint'),
42
+ title: z.string(),
43
+ description: z.string(),
44
+ evidence: z.array(z.string()),
45
+ });
46
+
47
+ // ===== RDF Vocabulary Constants =====
48
+
49
+ const KGC_NS = 'http://unrdf.dev/kgc#';
50
+ const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
51
+ const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
52
+ const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#';
53
+
54
+ /**
55
+ * Create a KGC namespace URI
56
+ * @param {string} localName - Local name in KGC namespace
57
+ * @returns {string} Full URI
58
+ */
59
+ function kgcUri(localName) {
60
+ return `${KGC_NS}${localName}`;
61
+ }
62
+
63
+ /**
64
+ * Generate deterministic hash for observation
65
+ * @param {Object} observation - Observation object
66
+ * @returns {string} SHA-256 hash
67
+ */
68
+ function generateHash(observation) {
69
+ if (observation.hash) return observation.hash;
70
+
71
+ const content = JSON.stringify({
72
+ method: observation.method,
73
+ outputs: observation.outputs,
74
+ timestamp: observation.timestamp,
75
+ });
76
+
77
+ return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
78
+ }
79
+
80
+ /**
81
+ * Convert observations array to RDF/Turtle format
82
+ *
83
+ * Each observation becomes a kgc:Observation resource with:
84
+ * - kgc:method (method name)
85
+ * - kgc:timestamp (ISO datetime)
86
+ * - kgc:hash (content hash for provenance)
87
+ * - kgc:outputs (JSON-serialized outputs)
88
+ * - kgc:guardDecision (if present)
89
+ * - kgc:error (if present)
90
+ * - kgc:domain (if present)
91
+ *
92
+ * @param {Array<Object>} observations - Array of observation objects
93
+ * @returns {string} Turtle-formatted RDF string
94
+ *
95
+ * @example
96
+ * const obs = [{ method: 'probeRuntime', outputs: { node: 'v18.19.0' } }];
97
+ * const turtle = observationsToRdf(obs);
98
+ * console.log(turtle); // @prefix kgc: <http://unrdf.dev/kgc#> . ...
99
+ */
100
+ export function observationsToRdf(observations) {
101
+ // Validate input
102
+ const validatedObs = z.array(ObservationSchema).parse(observations);
103
+
104
+ // Sort observations by method name for deterministic output
105
+ const sortedObs = [...validatedObs].sort((a, b) =>
106
+ a.method.localeCompare(b.method)
107
+ );
108
+
109
+ // Create RDF store
110
+ const store = createStore();
111
+
112
+ // Add observations as RDF quads
113
+ for (const obs of sortedObs) {
114
+ const hash = generateHash(obs);
115
+ const obsUri = dataFactory.namedNode(kgcUri(`observation/${hash}`));
116
+ const timestamp = obs.timestamp || Date.now();
117
+
118
+ // Type declaration
119
+ store.add(dataFactory.quad(
120
+ obsUri,
121
+ dataFactory.namedNode(`${RDF_NS}type`),
122
+ dataFactory.namedNode(kgcUri('Observation'))
123
+ ));
124
+
125
+ // Method
126
+ store.add(dataFactory.quad(
127
+ obsUri,
128
+ dataFactory.namedNode(kgcUri('method')),
129
+ dataFactory.literal(obs.method)
130
+ ));
131
+
132
+ // Timestamp (as xsd:dateTime)
133
+ const timestampDate = new Date(timestamp).toISOString();
134
+ store.add(dataFactory.quad(
135
+ obsUri,
136
+ dataFactory.namedNode(kgcUri('timestamp')),
137
+ dataFactory.literal(timestampDate, dataFactory.namedNode(`${XSD_NS}dateTime`))
138
+ ));
139
+
140
+ // Hash
141
+ store.add(dataFactory.quad(
142
+ obsUri,
143
+ dataFactory.namedNode(kgcUri('hash')),
144
+ dataFactory.literal(hash, dataFactory.namedNode(`${XSD_NS}string`))
145
+ ));
146
+
147
+ // Outputs (as JSON)
148
+ store.add(dataFactory.quad(
149
+ obsUri,
150
+ dataFactory.namedNode(kgcUri('outputs')),
151
+ dataFactory.literal(JSON.stringify(obs.outputs), dataFactory.namedNode(`${RDF_NS}JSON`))
152
+ ));
153
+
154
+ // Optional: Guard decision
155
+ if (obs.guardDecision) {
156
+ store.add(dataFactory.quad(
157
+ obsUri,
158
+ dataFactory.namedNode(kgcUri('guardDecision')),
159
+ dataFactory.literal(obs.guardDecision, dataFactory.namedNode(`${XSD_NS}string`))
160
+ ));
161
+ }
162
+
163
+ // Optional: Error
164
+ if (obs.error) {
165
+ store.add(dataFactory.quad(
166
+ obsUri,
167
+ dataFactory.namedNode(kgcUri('error')),
168
+ dataFactory.literal(obs.error, dataFactory.namedNode(`${XSD_NS}string`))
169
+ ));
170
+ }
171
+
172
+ // Optional: Domain
173
+ if (obs.domain) {
174
+ store.add(dataFactory.quad(
175
+ obsUri,
176
+ dataFactory.namedNode(kgcUri('domain')),
177
+ dataFactory.literal(obs.domain, dataFactory.namedNode(`${XSD_NS}string`))
178
+ ));
179
+ }
180
+ }
181
+
182
+ // Serialize to Turtle
183
+ return store.dump({ format: 'turtle' });
184
+ }
185
+
186
+ /**
187
+ * Derive high-level capability and constraint claims from observations
188
+ *
189
+ * Analyzes observation patterns to extract:
190
+ * - Capabilities: Features/resources available in the environment
191
+ * - Constraints: Limitations or restrictions discovered
192
+ *
193
+ * @param {Array<Object>} observations - Array of observation objects
194
+ * @returns {Object} Object with capabilities and constraints arrays
195
+ *
196
+ * @example
197
+ * const claims = deriveClaims(observations);
198
+ * console.log(claims.capabilities); // [{ type: 'capability', title: '...', ... }]
199
+ * console.log(claims.constraints); // [{ type: 'constraint', title: '...', ... }]
200
+ */
201
+ export function deriveClaims(observations) {
202
+ // Validate input
203
+ const validatedObs = z.array(ObservationSchema).parse(observations);
204
+
205
+ const capabilities = [];
206
+ const constraints = [];
207
+
208
+ // Group observations by domain
209
+ const byDomain = new Map();
210
+ for (const obs of validatedObs) {
211
+ const domain = obs.domain || 'general';
212
+ if (!byDomain.has(domain)) {
213
+ byDomain.set(domain, []);
214
+ }
215
+ byDomain.get(domain).push(obs);
216
+ }
217
+
218
+ // Analyze runtime observations
219
+ const runtimeObs = validatedObs.filter(o =>
220
+ o.method?.toLowerCase().includes('runtime') ||
221
+ o.domain === 'runtime'
222
+ );
223
+
224
+ if (runtimeObs.length > 0) {
225
+ const evidence = runtimeObs.map(o => o.method);
226
+ const outputs = runtimeObs.map(o => o.outputs).filter(Boolean);
227
+
228
+ // Extract Node.js version if present
229
+ const nodeVersion = outputs.find(o => o.node || o.version)?.node ||
230
+ outputs.find(o => o.node || o.version)?.version;
231
+
232
+ if (nodeVersion) {
233
+ capabilities.push({
234
+ type: 'capability',
235
+ title: `Node.js Runtime ${nodeVersion}`,
236
+ description: `Runtime environment running Node.js version ${nodeVersion}`,
237
+ evidence,
238
+ });
239
+ }
240
+
241
+ // Check for worker_threads
242
+ const workerThreads = outputs.some(o =>
243
+ o.worker_threads === true ||
244
+ o.workerThreads === true ||
245
+ (typeof o === 'object' && 'worker_threads' in o)
246
+ );
247
+
248
+ if (workerThreads) {
249
+ capabilities.push({
250
+ type: 'capability',
251
+ title: 'Worker Threads Available',
252
+ description: 'Runtime supports worker_threads for parallel execution',
253
+ evidence,
254
+ });
255
+ }
256
+ }
257
+
258
+ // Analyze WASM observations
259
+ const wasmObs = validatedObs.filter(o =>
260
+ o.method?.toLowerCase().includes('wasm') ||
261
+ o.domain === 'wasm'
262
+ );
263
+
264
+ if (wasmObs.length > 0) {
265
+ const evidence = wasmObs.map(o => o.method);
266
+ const outputs = wasmObs.map(o => o.outputs).filter(Boolean);
267
+
268
+ const wasmAvailable = outputs.some(o => o.available === true || o.wasm === true);
269
+
270
+ if (wasmAvailable) {
271
+ const maxMemory = outputs.find(o => o.maxMemory)?.maxMemory;
272
+ const memoryDesc = maxMemory ? ` with maximum memory ${maxMemory}` : '';
273
+
274
+ capabilities.push({
275
+ type: 'capability',
276
+ title: 'WebAssembly Support',
277
+ description: `WASM compilation and execution available${memoryDesc}`,
278
+ evidence,
279
+ });
280
+ }
281
+ }
282
+
283
+ // Analyze filesystem observations
284
+ const fsObs = validatedObs.filter(o =>
285
+ o.method?.toLowerCase().includes('filesystem') ||
286
+ o.method?.toLowerCase().includes('fs') ||
287
+ o.domain === 'filesystem'
288
+ );
289
+
290
+ if (fsObs.length > 0) {
291
+ const evidence = fsObs.map(o => o.method);
292
+ const outputs = fsObs.map(o => o.outputs).filter(Boolean);
293
+
294
+ // Check for restricted paths
295
+ const allowedPaths = outputs.find(o => o.allowedPaths || o.paths)?.allowedPaths ||
296
+ outputs.find(o => o.allowedPaths || o.paths)?.paths;
297
+
298
+ if (allowedPaths && Array.isArray(allowedPaths)) {
299
+ constraints.push({
300
+ type: 'constraint',
301
+ title: 'Filesystem Access Restricted',
302
+ description: `Filesystem access limited to: ${allowedPaths.join(', ')}`,
303
+ evidence,
304
+ });
305
+ }
306
+
307
+ // Check for denied operations
308
+ const deniedOps = fsObs.filter(o =>
309
+ o.guardDecision === 'denied' ||
310
+ o.error?.includes('permission') ||
311
+ o.error?.includes('denied')
312
+ );
313
+
314
+ if (deniedOps.length > 0) {
315
+ constraints.push({
316
+ type: 'constraint',
317
+ title: 'Filesystem Operations Denied',
318
+ description: `${deniedOps.length} filesystem operation(s) were denied by security guards`,
319
+ evidence: deniedOps.map(o => o.method),
320
+ });
321
+ }
322
+ }
323
+
324
+ // Analyze network observations
325
+ const networkObs = validatedObs.filter(o =>
326
+ o.method?.toLowerCase().includes('network') ||
327
+ o.domain === 'network'
328
+ );
329
+
330
+ if (networkObs.length > 0) {
331
+ const evidence = networkObs.map(o => o.method);
332
+ const outputs = networkObs.map(o => o.outputs).filter(Boolean);
333
+
334
+ // Check for allowlisted URLs
335
+ const allowedUrls = outputs.find(o => o.allowedUrls || o.urls)?.allowedUrls ||
336
+ outputs.find(o => o.allowedUrls || o.urls)?.urls;
337
+
338
+ if (allowedUrls && Array.isArray(allowedUrls)) {
339
+ constraints.push({
340
+ type: 'constraint',
341
+ title: 'Network Access Allowlist',
342
+ description: `Network requests restricted to ${allowedUrls.length} allowlisted URL(s)`,
343
+ evidence,
344
+ });
345
+ }
346
+
347
+ // Check for denied requests
348
+ const deniedReqs = networkObs.filter(o =>
349
+ o.guardDecision === 'denied' ||
350
+ o.error?.includes('network') ||
351
+ o.error?.includes('blocked')
352
+ );
353
+
354
+ if (deniedReqs.length > 0) {
355
+ constraints.push({
356
+ type: 'constraint',
357
+ title: 'Network Requests Blocked',
358
+ description: `${deniedReqs.length} network request(s) were blocked`,
359
+ evidence: deniedReqs.map(o => o.method),
360
+ });
361
+ }
362
+ }
363
+
364
+ // Analyze performance observations
365
+ const perfObs = validatedObs.filter(o =>
366
+ o.method?.toLowerCase().includes('performance') ||
367
+ o.method?.toLowerCase().includes('benchmark') ||
368
+ o.domain === 'performance'
369
+ );
370
+
371
+ if (perfObs.length > 0) {
372
+ const evidence = perfObs.map(o => o.method);
373
+ const outputs = perfObs.map(o => o.outputs).filter(Boolean);
374
+
375
+ // Look for execution times
376
+ const executionTimes = outputs
377
+ .map(o => o.executionTime || o.duration || o.time)
378
+ .filter(t => typeof t === 'number');
379
+
380
+ if (executionTimes.length > 0) {
381
+ const avgTime = executionTimes.reduce((a, b) => a + b, 0) / executionTimes.length;
382
+ capabilities.push({
383
+ type: 'capability',
384
+ title: 'Performance Metrics',
385
+ description: `Average execution time: ${avgTime.toFixed(2)}ms across ${executionTimes.length} measurement(s)`,
386
+ evidence,
387
+ });
388
+ }
389
+ }
390
+
391
+ // Validate outputs
392
+ const validatedCapabilities = capabilities.map(c => CapabilitySchema.parse(c));
393
+ const validatedConstraints = constraints.map(c => ConstraintSchema.parse(c));
394
+
395
+ return {
396
+ capabilities: validatedCapabilities,
397
+ constraints: validatedConstraints,
398
+ };
399
+ }
400
+
401
+ /**
402
+ * Generate comprehensive Markdown report from observations
403
+ *
404
+ * Creates a structured report with:
405
+ * - Executive summary with counts
406
+ * - Discovered capabilities
407
+ * - Detected constraints
408
+ * - Observations grouped by domain
409
+ *
410
+ * @param {Array<Object>} observations - Array of observation objects
411
+ * @returns {string} Markdown-formatted report
412
+ *
413
+ * @example
414
+ * const report = generateReport(observations);
415
+ * console.log(report); // # KGC Probe Report\n\n## Summary\n...
416
+ */
417
+ export function generateReport(observations) {
418
+ // Validate input
419
+ const validatedObs = z.array(ObservationSchema).parse(observations);
420
+
421
+ // Derive claims
422
+ const { capabilities, constraints } = deriveClaims(validatedObs);
423
+
424
+ // Calculate execution time range
425
+ const timestamps = validatedObs
426
+ .map(o => o.timestamp)
427
+ .filter(t => typeof t === 'number')
428
+ .sort((a, b) => a - b);
429
+
430
+ const executionTime = timestamps.length >= 2
431
+ ? timestamps[timestamps.length - 1] - timestamps[0]
432
+ : 0;
433
+
434
+ // Group observations by domain
435
+ const byDomain = new Map();
436
+ for (const obs of validatedObs) {
437
+ const domain = obs.domain || 'general';
438
+ if (!byDomain.has(domain)) {
439
+ byDomain.set(domain, []);
440
+ }
441
+ byDomain.get(domain).push(obs);
442
+ }
443
+
444
+ // Sort domains alphabetically
445
+ const sortedDomains = Array.from(byDomain.keys()).sort();
446
+
447
+ // Build report
448
+ let report = '# KGC Probe Report\n\n';
449
+
450
+ // Summary section
451
+ report += '## Summary\n\n';
452
+ report += `- **Total observations**: ${validatedObs.length}\n`;
453
+ report += `- **Capabilities discovered**: ${capabilities.length}\n`;
454
+ report += `- **Constraints detected**: ${constraints.length}\n`;
455
+ report += `- **Execution time**: ${executionTime}ms\n`;
456
+ report += `- **Domains probed**: ${sortedDomains.length}\n`;
457
+ report += '\n';
458
+
459
+ // Capabilities section
460
+ if (capabilities.length > 0) {
461
+ report += '## Capabilities\n\n';
462
+ report += 'The following capabilities were discovered in the runtime environment:\n\n';
463
+
464
+ for (const capability of capabilities) {
465
+ report += `### ${capability.title}\n\n`;
466
+ report += `${capability.description}\n\n`;
467
+ report += `**Evidence**: ${capability.evidence.join(', ')}\n\n`;
468
+ }
469
+ } else {
470
+ report += '## Capabilities\n\n';
471
+ report += '*No capabilities detected*\n\n';
472
+ }
473
+
474
+ // Constraints section
475
+ if (constraints.length > 0) {
476
+ report += '## Constraints\n\n';
477
+ report += 'The following constraints and limitations were detected:\n\n';
478
+
479
+ for (const constraint of constraints) {
480
+ report += `### ${constraint.title}\n\n`;
481
+ report += `${constraint.description}\n\n`;
482
+ report += `**Evidence**: ${constraint.evidence.join(', ')}\n\n`;
483
+ }
484
+ } else {
485
+ report += '## Constraints\n\n';
486
+ report += '*No constraints detected*\n\n';
487
+ }
488
+
489
+ // Observations by domain
490
+ report += '## Observations by Domain\n\n';
491
+
492
+ for (const domain of sortedDomains) {
493
+ const domainObs = byDomain.get(domain);
494
+ report += `### ${domain.charAt(0).toUpperCase() + domain.slice(1)} (${domainObs.length} observations)\n\n`;
495
+
496
+ // Sort observations by method name
497
+ const sortedDomainObs = [...domainObs].sort((a, b) =>
498
+ a.method.localeCompare(b.method)
499
+ );
500
+
501
+ for (const obs of sortedDomainObs) {
502
+ report += `#### ${obs.method}\n\n`;
503
+
504
+ // Guard decision
505
+ if (obs.guardDecision) {
506
+ report += `**Guard Decision**: ${obs.guardDecision}\n\n`;
507
+ }
508
+
509
+ // Outputs
510
+ if (obs.outputs) {
511
+ report += '**Outputs**:\n```json\n';
512
+ report += JSON.stringify(obs.outputs, null, 2);
513
+ report += '\n```\n\n';
514
+ }
515
+
516
+ // Error
517
+ if (obs.error) {
518
+ report += `**Error**: ${obs.error}\n\n`;
519
+ }
520
+
521
+ // Timestamp
522
+ if (obs.timestamp) {
523
+ const timestampDate = new Date(obs.timestamp).toISOString();
524
+ report += `**Timestamp**: ${timestampDate}\n\n`;
525
+ }
526
+
527
+ // Hash
528
+ const hash = generateHash(obs);
529
+ report += `**Hash**: ${hash}\n\n`;
530
+
531
+ report += '---\n\n';
532
+ }
533
+ }
534
+
535
+ // Footer
536
+ report += `\n*Report generated at ${new Date().toISOString()}*\n`;
537
+
538
+ return report;
539
+ }
540
+
541
+ // Export schemas for external use
542
+ export {
543
+ ObservationSchema,
544
+ CapabilitySchema,
545
+ ConstraintSchema,
546
+ };
547
+
548
+ export default {
549
+ observationsToRdf,
550
+ generateReport,
551
+ deriveClaims,
552
+ ObservationSchema,
553
+ CapabilitySchema,
554
+ ConstraintSchema,
555
+ };