@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,383 @@
1
+ /**
2
+ * @fileoverview RDF/Turtle converter for KGC Probe observations
3
+ *
4
+ * Converts observations to RDF/Turtle format with capability and constraint derivation.
5
+ * Uses the observation schema from orchestrator (category, severity, message format).
6
+ *
7
+ * Design principles:
8
+ * - Deterministic: Same observations → same Turtle output
9
+ * - Provenance: Links capabilities/constraints to observation hashes
10
+ * - Minimal vocabulary: kgc:Observation, kgc:Capability, kgc:Constraint
11
+ */
12
+
13
+ import { createStore, dataFactory } from '@unrdf/oxigraph';
14
+ import crypto from 'crypto';
15
+
16
+ // ===== RDF Vocabulary Constants =====
17
+
18
+ const KGC_NS = 'https://unrdf.org/kgc/probe#';
19
+ const XSD_NS = 'http://www.w3.org/2001/XMLSchema#';
20
+ const RDF_NS = 'http://www.w3.org/1999/02/22-rdf-syntax-ns#';
21
+ const RDFS_NS = 'http://www.w3.org/2000/01/rdf-schema#';
22
+
23
+ /**
24
+ * Create a KGC namespace URI
25
+ *
26
+ * @param {string} localName - Local name in KGC namespace
27
+ * @returns {string} Full URI
28
+ */
29
+ function kgcUri(localName) {
30
+ return `${KGC_NS}${localName}`;
31
+ }
32
+
33
+ /**
34
+ * Generate deterministic hash for observation
35
+ *
36
+ * @param {Object} observation - Observation object
37
+ * @returns {string} SHA-256 hash (first 16 chars)
38
+ */
39
+ function generateHash(observation) {
40
+ if (observation.hash) return observation.hash;
41
+ if (observation.receiptHash) return observation.receiptHash;
42
+
43
+ const content = JSON.stringify({
44
+ method: observation.method,
45
+ category: observation.category,
46
+ message: observation.message,
47
+ outputs: observation.outputs || observation.data,
48
+ timestamp: observation.timestamp || observation.metadata?.timestamp,
49
+ });
50
+
51
+ return crypto.createHash('sha256').update(content).digest('hex').substring(0, 16);
52
+ }
53
+
54
+ /**
55
+ * Derive capabilities from observations
56
+ *
57
+ * Capability = feature/resource available in the environment
58
+ * Derives when:
59
+ * - observation.outputs.available === true
60
+ * - observation.data indicates successful feature detection
61
+ * - No error present
62
+ *
63
+ * @param {Array<Object>} observations - Array of observations
64
+ * @returns {Array<Object>} Capabilities with provenance
65
+ */
66
+ function deriveCapabilities(observations) {
67
+ const capabilities = [];
68
+
69
+ // Group by domain/method for analysis
70
+ for (const obs of observations) {
71
+ const hash = generateHash(obs);
72
+ const data = obs.outputs || obs.data || {};
73
+
74
+ // Check for explicit availability flag
75
+ if (data.available === true) {
76
+ const domain = obs.domain || obs.category || 'unknown';
77
+ const method = obs.method || obs.message || 'unknown';
78
+
79
+ capabilities.push({
80
+ name: `${domain}.${method}`,
81
+ available: true,
82
+ derivedFrom: [`urn:kgc:obs:${hash}`],
83
+ data
84
+ });
85
+ }
86
+
87
+ // Check for specific capability indicators
88
+ if (data.worker_threads === true || data.workerThreads === true) {
89
+ capabilities.push({
90
+ name: 'concurrency.worker_threads',
91
+ available: true,
92
+ derivedFrom: [`urn:kgc:obs:${hash}`],
93
+ data: { module: 'worker_threads' }
94
+ });
95
+ }
96
+
97
+ if (data.wasm === true || data.WebAssembly === true) {
98
+ capabilities.push({
99
+ name: 'runtime.wasm',
100
+ available: true,
101
+ derivedFrom: [`urn:kgc:obs:${hash}`],
102
+ data: { support: 'available' }
103
+ });
104
+ }
105
+ }
106
+
107
+ return capabilities;
108
+ }
109
+
110
+ /**
111
+ * Derive constraints from observations
112
+ *
113
+ * Constraint = limitation/boundary discovered in the environment
114
+ * Derives when:
115
+ * - observation shows guard denial
116
+ * - observation shows error/limit
117
+ * - observation shows restricted access
118
+ *
119
+ * @param {Array<Object>} observations - Array of observations
120
+ * @returns {Array<Object>} Constraints with provenance
121
+ */
122
+ function deriveConstraints(observations) {
123
+ const constraints = [];
124
+
125
+ for (const obs of observations) {
126
+ const hash = generateHash(obs);
127
+ const data = obs.outputs || obs.data || {};
128
+
129
+ // Check for guard denials
130
+ if (obs.guardDecision === 'denied' || obs.category === 'guard' || data.guardDecision === 'denied') {
131
+ constraints.push({
132
+ type: 'guard-denial',
133
+ description: obs.message || 'Operation denied by guard',
134
+ derivedFrom: [`urn:kgc:obs:${hash}`],
135
+ data: { guard: data.guardName || 'unknown' }
136
+ });
137
+ }
138
+
139
+ // Check for errors indicating limits
140
+ if (obs.error || data.error) {
141
+ constraints.push({
142
+ type: 'error-boundary',
143
+ description: obs.error || data.error || 'Error encountered',
144
+ derivedFrom: [`urn:kgc:obs:${hash}`],
145
+ data: { errorType: data.errorType || 'unknown' }
146
+ });
147
+ }
148
+
149
+ // Check for explicit limits
150
+ if (data.maxMemory !== undefined) {
151
+ constraints.push({
152
+ type: 'memory-limit',
153
+ description: `Maximum memory: ${data.maxMemory}`,
154
+ derivedFrom: [`urn:kgc:obs:${hash}`],
155
+ data: { limit: data.maxMemory }
156
+ });
157
+ }
158
+
159
+ if (data.maxStackDepth !== undefined) {
160
+ constraints.push({
161
+ type: 'stack-depth-limit',
162
+ description: `Maximum stack depth: ${data.maxStackDepth}`,
163
+ derivedFrom: [`urn:kgc:obs:${hash}`],
164
+ data: { limit: data.maxStackDepth }
165
+ });
166
+ }
167
+ }
168
+
169
+ return constraints;
170
+ }
171
+
172
+ /**
173
+ * Convert observations to RDF/Turtle format
174
+ *
175
+ * Generates Turtle with:
176
+ * - kgc:Observation for each observation
177
+ * - kgc:Capability for derived capabilities
178
+ * - kgc:Constraint for derived constraints
179
+ * - Provenance links via kgc:derivedFrom
180
+ *
181
+ * @param {Array<Object>} observations - Array of observation objects
182
+ * @returns {Promise<string>} Turtle-formatted RDF string
183
+ *
184
+ * @example
185
+ * const obs = [{ method: 'runtime.node-version', outputs: { version: 'v22.21.1' } }];
186
+ * const turtle = await convertToTurtle(obs);
187
+ * console.log(turtle); // @prefix kgc: <https://unrdf.org/kgc/probe#> . ...
188
+ */
189
+ export async function convertToTurtle(observations) {
190
+ // Create RDF store
191
+ const store = createStore();
192
+
193
+ // Sort observations by hash for deterministic output
194
+ const sortedObs = [...observations].sort((a, b) => {
195
+ const hashA = generateHash(a);
196
+ const hashB = generateHash(b);
197
+ return hashA.localeCompare(hashB);
198
+ });
199
+
200
+ // Add observations as RDF quads
201
+ for (const obs of sortedObs) {
202
+ const hash = generateHash(obs);
203
+ const obsUri = dataFactory.namedNode(`urn:kgc:obs:${hash}`);
204
+
205
+ // Type declaration
206
+ store.add(dataFactory.quad(
207
+ obsUri,
208
+ dataFactory.namedNode(`${RDF_NS}type`),
209
+ dataFactory.namedNode(kgcUri('Observation'))
210
+ ));
211
+
212
+ // Domain (category or inferred from method)
213
+ const domain = obs.domain || obs.category || 'unknown';
214
+ store.add(dataFactory.quad(
215
+ obsUri,
216
+ dataFactory.namedNode(kgcUri('domain')),
217
+ dataFactory.literal(domain)
218
+ ));
219
+
220
+ // Method (from method field or message)
221
+ const method = obs.method || obs.message || 'unknown';
222
+ store.add(dataFactory.quad(
223
+ obsUri,
224
+ dataFactory.namedNode(kgcUri('method')),
225
+ dataFactory.literal(method)
226
+ ));
227
+
228
+ // Timestamp
229
+ const timestamp = obs.timestamp || obs.metadata?.timestamp || Date.now();
230
+ const timestampDate = typeof timestamp === 'number'
231
+ ? new Date(timestamp).toISOString()
232
+ : timestamp;
233
+
234
+ store.add(dataFactory.quad(
235
+ obsUri,
236
+ dataFactory.namedNode(kgcUri('timestamp')),
237
+ dataFactory.literal(timestampDate, dataFactory.namedNode(`${XSD_NS}dateTime`))
238
+ ));
239
+
240
+ // Hash
241
+ store.add(dataFactory.quad(
242
+ obsUri,
243
+ dataFactory.namedNode(kgcUri('hash')),
244
+ dataFactory.literal(hash)
245
+ ));
246
+
247
+ // Outputs (serialize data/outputs as JSON)
248
+ const outputs = obs.outputs || obs.data || {};
249
+ store.add(dataFactory.quad(
250
+ obsUri,
251
+ dataFactory.namedNode(kgcUri('outputs')),
252
+ dataFactory.literal(JSON.stringify(outputs), dataFactory.namedNode(`${RDF_NS}JSON`))
253
+ ));
254
+
255
+ // Optional: Guard decision
256
+ if (obs.guardDecision) {
257
+ store.add(dataFactory.quad(
258
+ obsUri,
259
+ dataFactory.namedNode(kgcUri('guardDecision')),
260
+ dataFactory.literal(obs.guardDecision)
261
+ ));
262
+ }
263
+
264
+ // Optional: Error
265
+ if (obs.error) {
266
+ store.add(dataFactory.quad(
267
+ obsUri,
268
+ dataFactory.namedNode(kgcUri('error')),
269
+ dataFactory.literal(obs.error)
270
+ ));
271
+ }
272
+
273
+ // Optional: Severity (if from orchestrator schema)
274
+ if (obs.severity) {
275
+ store.add(dataFactory.quad(
276
+ obsUri,
277
+ dataFactory.namedNode(kgcUri('severity')),
278
+ dataFactory.literal(obs.severity)
279
+ ));
280
+ }
281
+ }
282
+
283
+ // Derive and add capabilities
284
+ const capabilities = deriveCapabilities(sortedObs);
285
+ for (let i = 0; i < capabilities.length; i++) {
286
+ const cap = capabilities[i];
287
+ const capUri = dataFactory.namedNode(`urn:kgc:cap:${i + 1}`);
288
+
289
+ store.add(dataFactory.quad(
290
+ capUri,
291
+ dataFactory.namedNode(`${RDF_NS}type`),
292
+ dataFactory.namedNode(kgcUri('Capability'))
293
+ ));
294
+
295
+ store.add(dataFactory.quad(
296
+ capUri,
297
+ dataFactory.namedNode(kgcUri('name')),
298
+ dataFactory.literal(cap.name)
299
+ ));
300
+
301
+ store.add(dataFactory.quad(
302
+ capUri,
303
+ dataFactory.namedNode(kgcUri('available')),
304
+ dataFactory.literal(String(cap.available), dataFactory.namedNode(`${XSD_NS}boolean`))
305
+ ));
306
+
307
+ // Link to observations
308
+ for (const obsRef of cap.derivedFrom) {
309
+ store.add(dataFactory.quad(
310
+ capUri,
311
+ dataFactory.namedNode(kgcUri('derivedFrom')),
312
+ dataFactory.namedNode(obsRef)
313
+ ));
314
+ }
315
+ }
316
+
317
+ // Derive and add constraints
318
+ const constraints = deriveConstraints(sortedObs);
319
+ for (let i = 0; i < constraints.length; i++) {
320
+ const constraint = constraints[i];
321
+ const constraintUri = dataFactory.namedNode(`urn:kgc:constraint:${i + 1}`);
322
+
323
+ store.add(dataFactory.quad(
324
+ constraintUri,
325
+ dataFactory.namedNode(`${RDF_NS}type`),
326
+ dataFactory.namedNode(kgcUri('Constraint'))
327
+ ));
328
+
329
+ store.add(dataFactory.quad(
330
+ constraintUri,
331
+ dataFactory.namedNode(kgcUri('constraintType')),
332
+ dataFactory.literal(constraint.type)
333
+ ));
334
+
335
+ store.add(dataFactory.quad(
336
+ constraintUri,
337
+ dataFactory.namedNode(kgcUri('description')),
338
+ dataFactory.literal(constraint.description)
339
+ ));
340
+
341
+ // Link to observations
342
+ for (const obsRef of constraint.derivedFrom) {
343
+ store.add(dataFactory.quad(
344
+ constraintUri,
345
+ dataFactory.namedNode(kgcUri('derivedFrom')),
346
+ dataFactory.namedNode(obsRef)
347
+ ));
348
+ }
349
+ }
350
+
351
+ // Serialize to Turtle using N3.js Writer
352
+ // Note: Oxigraph dump() may not support 'turtle' format in all versions
353
+ // So we extract quads and use N3.js Writer for compatibility
354
+ const N3 = await import('n3');
355
+ const writer = new N3.Writer({ format: 'Turtle' });
356
+
357
+ // Get all quads from store
358
+ const quads = store.match();
359
+
360
+ // Add quads to writer
361
+ for (const quad of quads) {
362
+ writer.addQuad(quad);
363
+ }
364
+
365
+ // Return serialized Turtle
366
+ return new Promise((resolve, reject) => {
367
+ writer.end((error, result) => {
368
+ if (error) reject(error);
369
+ else resolve(result);
370
+ });
371
+ });
372
+ }
373
+
374
+ export {
375
+ deriveCapabilities,
376
+ deriveConstraints
377
+ };
378
+
379
+ export default {
380
+ convertToTurtle,
381
+ deriveCapabilities,
382
+ deriveConstraints
383
+ };