@net-mesh/sdk 0.19.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,112 @@
1
+ /**
2
+ * Capability axis schema + `validateCapabilities` — Phase 9a of
3
+ * `CAPABILITY_SYSTEM_SDK_PLAN.md`.
4
+ *
5
+ * Mirrors the substrate's `AXIS_SCHEMA` const + the canonical
6
+ * `validate_capabilities` validator. The wire shape of the
7
+ * `ValidationReport` (lowercase `kind` discriminator, axis as
8
+ * lowercase string, value type as lowercase string) is pinned by the
9
+ * cross-binding fixture `tests/cross_lang_capability/capability_validation.json`.
10
+ *
11
+ * Source-of-truth for the schema is
12
+ * `net/crates/net/docs/CAPABILITIES_SCHEMA.md`. The substrate's Rust
13
+ * mirror in `behavior/schema.rs` and this TS mirror are
14
+ * hand-maintained against the doc; CI guards each binding's
15
+ * regenerated schema once the codegen tool from Phase 9a's plan
16
+ * lands.
17
+ *
18
+ * @packageDocumentation
19
+ */
20
+ import type { CapabilitySetWire, TaxonomyAxis } from './capability-enhancements';
21
+ export type ValueType = 'presence' | 'number' | 'string' | 'enumeration' | 'bool' | 'csv';
22
+ export interface KeyEntry {
23
+ /** Full key under its axis, e.g. `cpu_cores`, `gpu.vendor`. */
24
+ key: string;
25
+ valueType: ValueType;
26
+ }
27
+ export type KeyShapeKind = {
28
+ kind: 'indexedCollection';
29
+ } | {
30
+ kind: 'keyedMap';
31
+ valueType: ValueType;
32
+ };
33
+ export interface KeyShape {
34
+ /** Axis-relative prefix, e.g. `model.` (trailing `.` mandatory). */
35
+ prefix: string;
36
+ shape: KeyShapeKind;
37
+ /** Per-element sub-keys for indexed collections; empty for keyed maps. */
38
+ subKeys: KeyEntry[];
39
+ }
40
+ export interface AxisEntry {
41
+ keys: KeyEntry[];
42
+ shapes: KeyShape[];
43
+ }
44
+ export interface AxisSchema {
45
+ hardware: AxisEntry;
46
+ software: AxisEntry;
47
+ devices: AxisEntry;
48
+ dataforts: AxisEntry;
49
+ metadataReserved: string[];
50
+ metadataReservedPrefixes: string[];
51
+ }
52
+ /** Reserved metadata keys (substrate-defined). */
53
+ export declare const METADATA_RESERVED_KEYS: readonly string[];
54
+ /** Reserved metadata-key prefixes (`tool::<id>::input_schema` etc.). */
55
+ export declare const METADATA_RESERVED_PREFIXES: readonly string[];
56
+ /** Default soft cap for `metadata` total size. */
57
+ export declare const METADATA_SOFT_CAP_BYTES: number;
58
+ /** The canonical axis schema. Mirrors `behavior::schema::AXIS_SCHEMA`. */
59
+ export declare const AXIS_SCHEMA: AxisSchema;
60
+ export type SchemaError = {
61
+ kind: 'unknown_axis';
62
+ axis_prefix: string;
63
+ tag: string;
64
+ } | {
65
+ kind: 'type_mismatch';
66
+ axis: TaxonomyAxis;
67
+ key: string;
68
+ expected: ValueType;
69
+ actual: string;
70
+ } | {
71
+ kind: 'index_malformed';
72
+ axis: TaxonomyAxis;
73
+ prefix: string;
74
+ index: string;
75
+ tag: string;
76
+ };
77
+ export type ValidationWarning = {
78
+ kind: 'unknown_key';
79
+ axis: TaxonomyAxis;
80
+ key: string;
81
+ } | {
82
+ kind: 'metadata_oversize';
83
+ soft_cap_bytes: number;
84
+ actual_bytes: number;
85
+ } | {
86
+ kind: 'legacy_tag';
87
+ tag: string;
88
+ } | {
89
+ kind: 'metadata_reserved_key';
90
+ key: string;
91
+ } | {
92
+ kind: 'metadata_reserved_prefix';
93
+ key: string;
94
+ prefix: string;
95
+ };
96
+ export interface ValidationReport {
97
+ errors: SchemaError[];
98
+ warnings: ValidationWarning[];
99
+ }
100
+ /**
101
+ * Validate a wire-format capability set against a schema. Defaults to
102
+ * the canonical {@link AXIS_SCHEMA}; pass a custom schema for
103
+ * application-specific extensions.
104
+ *
105
+ * Mirrors the substrate's `validate_capabilities`. Output shape pinned
106
+ * by `tests/cross_lang_capability/capability_validation.json`.
107
+ */
108
+ export declare function validateCapabilities(caps: CapabilitySetWire, schema?: AxisSchema): ValidationReport;
109
+ /** True iff there are zero errors and zero warnings. */
110
+ export declare function isReportClean(r: ValidationReport): boolean;
111
+ /** True iff there are zero errors. Warnings are allowed. */
112
+ export declare function isReportValid(r: ValidationReport): boolean;
@@ -0,0 +1,317 @@
1
+ "use strict";
2
+ /**
3
+ * Capability axis schema + `validateCapabilities` — Phase 9a of
4
+ * `CAPABILITY_SYSTEM_SDK_PLAN.md`.
5
+ *
6
+ * Mirrors the substrate's `AXIS_SCHEMA` const + the canonical
7
+ * `validate_capabilities` validator. The wire shape of the
8
+ * `ValidationReport` (lowercase `kind` discriminator, axis as
9
+ * lowercase string, value type as lowercase string) is pinned by the
10
+ * cross-binding fixture `tests/cross_lang_capability/capability_validation.json`.
11
+ *
12
+ * Source-of-truth for the schema is
13
+ * `net/crates/net/docs/CAPABILITIES_SCHEMA.md`. The substrate's Rust
14
+ * mirror in `behavior/schema.rs` and this TS mirror are
15
+ * hand-maintained against the doc; CI guards each binding's
16
+ * regenerated schema once the codegen tool from Phase 9a's plan
17
+ * lands.
18
+ *
19
+ * @packageDocumentation
20
+ */
21
+ Object.defineProperty(exports, "__esModule", { value: true });
22
+ exports.AXIS_SCHEMA = exports.METADATA_SOFT_CAP_BYTES = exports.METADATA_RESERVED_PREFIXES = exports.METADATA_RESERVED_KEYS = void 0;
23
+ exports.validateCapabilities = validateCapabilities;
24
+ exports.isReportClean = isReportClean;
25
+ exports.isReportValid = isReportValid;
26
+ const capability_enhancements_1 = require("./capability-enhancements");
27
+ // ============================================================================
28
+ // AXIS_SCHEMA — mirrors `behavior::schema::AXIS_SCHEMA`.
29
+ // ============================================================================
30
+ const HARDWARE_KEYS = [
31
+ { key: 'cpu_cores', valueType: 'number' },
32
+ { key: 'cpu_threads', valueType: 'number' },
33
+ { key: 'memory_gb', valueType: 'number' },
34
+ { key: 'gpu', valueType: 'presence' },
35
+ { key: 'gpu.vendor', valueType: 'enumeration' },
36
+ { key: 'gpu.model', valueType: 'string' },
37
+ { key: 'gpu.vram_gb', valueType: 'number' },
38
+ { key: 'gpu.compute_units', valueType: 'number' },
39
+ { key: 'gpu.tensor_cores', valueType: 'number' },
40
+ { key: 'gpu.fp16_tflops_x10', valueType: 'number' },
41
+ { key: 'storage_gb', valueType: 'number' },
42
+ { key: 'network_gbps', valueType: 'number' },
43
+ { key: 'limits.max_concurrent_requests', valueType: 'number' },
44
+ { key: 'limits.max_tokens_per_request', valueType: 'number' },
45
+ { key: 'limits.rate_limit_rpm', valueType: 'number' },
46
+ { key: 'limits.max_batch_size', valueType: 'number' },
47
+ { key: 'limits.max_input_bytes', valueType: 'number' },
48
+ { key: 'limits.max_output_bytes', valueType: 'number' },
49
+ ];
50
+ const SOFTWARE_KEYS = [
51
+ { key: 'os', valueType: 'string' },
52
+ { key: 'os_version', valueType: 'string' },
53
+ { key: 'cuda_version', valueType: 'string' },
54
+ ];
55
+ const SOFTWARE_SHAPES = [
56
+ {
57
+ prefix: 'runtime.',
58
+ shape: { kind: 'keyedMap', valueType: 'string' },
59
+ subKeys: [],
60
+ },
61
+ {
62
+ prefix: 'framework.',
63
+ shape: { kind: 'keyedMap', valueType: 'string' },
64
+ subKeys: [],
65
+ },
66
+ {
67
+ prefix: 'driver.',
68
+ shape: { kind: 'keyedMap', valueType: 'string' },
69
+ subKeys: [],
70
+ },
71
+ {
72
+ prefix: 'model.',
73
+ shape: { kind: 'indexedCollection' },
74
+ subKeys: [
75
+ { key: 'id', valueType: 'string' },
76
+ { key: 'family', valueType: 'string' },
77
+ { key: 'parameters_b_x10', valueType: 'number' },
78
+ { key: 'context_length', valueType: 'number' },
79
+ { key: 'quantization', valueType: 'string' },
80
+ { key: 'modalities', valueType: 'csv' },
81
+ { key: 'tokens_per_sec', valueType: 'number' },
82
+ { key: 'loaded', valueType: 'bool' },
83
+ ],
84
+ },
85
+ {
86
+ prefix: 'tool.',
87
+ shape: { kind: 'indexedCollection' },
88
+ subKeys: [
89
+ { key: 'tool_id', valueType: 'string' },
90
+ { key: 'name', valueType: 'string' },
91
+ { key: 'version', valueType: 'string' },
92
+ { key: 'requires', valueType: 'csv' },
93
+ { key: 'estimated_time_ms', valueType: 'number' },
94
+ { key: 'stateless', valueType: 'bool' },
95
+ ],
96
+ },
97
+ ];
98
+ /** Reserved metadata keys (substrate-defined). */
99
+ exports.METADATA_RESERVED_KEYS = [
100
+ 'intent',
101
+ 'colocate-with',
102
+ 'colocate-with-strict',
103
+ 'priority',
104
+ 'owner',
105
+ ];
106
+ /** Reserved metadata-key prefixes (`tool::<id>::input_schema` etc.). */
107
+ exports.METADATA_RESERVED_PREFIXES = ['tool::'];
108
+ /** Default soft cap for `metadata` total size. */
109
+ exports.METADATA_SOFT_CAP_BYTES = 4 * 1024;
110
+ /** The canonical axis schema. Mirrors `behavior::schema::AXIS_SCHEMA`. */
111
+ exports.AXIS_SCHEMA = {
112
+ hardware: { keys: HARDWARE_KEYS, shapes: [] },
113
+ software: { keys: SOFTWARE_KEYS, shapes: SOFTWARE_SHAPES },
114
+ devices: { keys: [], shapes: [] },
115
+ dataforts: { keys: [], shapes: [] },
116
+ metadataReserved: [...exports.METADATA_RESERVED_KEYS],
117
+ metadataReservedPrefixes: [...exports.METADATA_RESERVED_PREFIXES],
118
+ };
119
+ // ============================================================================
120
+ // Validator
121
+ // ============================================================================
122
+ function axisEntry(schema, axis) {
123
+ switch (axis) {
124
+ case 'hardware':
125
+ return schema.hardware;
126
+ case 'software':
127
+ return schema.software;
128
+ case 'devices':
129
+ return schema.devices;
130
+ case 'dataforts':
131
+ return schema.dataforts;
132
+ }
133
+ }
134
+ function checkValue(entry, observedType, observedValue, axis, errors) {
135
+ if (entry.valueType === 'presence') {
136
+ if (observedType !== 'presence') {
137
+ errors.push({
138
+ kind: 'type_mismatch',
139
+ axis,
140
+ key: entry.key,
141
+ expected: 'presence',
142
+ actual: observedValue ?? '',
143
+ });
144
+ }
145
+ return;
146
+ }
147
+ if (observedValue === undefined) {
148
+ errors.push({
149
+ kind: 'type_mismatch',
150
+ axis,
151
+ key: entry.key,
152
+ expected: entry.valueType,
153
+ actual: '<no value>',
154
+ });
155
+ return;
156
+ }
157
+ let parses = false;
158
+ switch (entry.valueType) {
159
+ case 'number':
160
+ // Substrate `Number` is unsigned (u64-only) — Rust uses
161
+ // `value.parse::<u64>()` (schema.rs:704). The accepted-set is
162
+ // ASCII digits with an optional leading `+`, bounded by
163
+ // `u64::MAX` (18446744073709551615). N-5: the pre-fix regex
164
+ // `^\d+$` admitted values exceeding u64::MAX (e.g.
165
+ // `18446744073709551616`) that the substrate rejects; mirror
166
+ // the Python `_U64_LITERAL` + `int(...) <= u64::MAX` shape so
167
+ // cross-binding fixture rows agree.
168
+ if (/^\+?[0-9]+$/.test(observedValue)) {
169
+ try {
170
+ parses = BigInt(observedValue) <= 0xffffffffffffffffn;
171
+ }
172
+ catch {
173
+ parses = false;
174
+ }
175
+ }
176
+ break;
177
+ case 'string':
178
+ case 'enumeration':
179
+ case 'csv':
180
+ parses = observedValue.length > 0;
181
+ break;
182
+ case 'bool':
183
+ parses = observedValue === 'true' || observedValue === 'false';
184
+ break;
185
+ }
186
+ if (!parses) {
187
+ errors.push({
188
+ kind: 'type_mismatch',
189
+ axis,
190
+ key: entry.key,
191
+ expected: entry.valueType,
192
+ actual: observedValue,
193
+ });
194
+ }
195
+ }
196
+ function validateAxisKey(axis, key, observedType, observedValue, schema, errors, warnings, tagWire) {
197
+ const entry = axisEntry(schema, axis);
198
+ const fixed = entry.keys.find((e) => e.key === key);
199
+ if (fixed) {
200
+ checkValue(fixed, observedType, observedValue, axis, errors);
201
+ return;
202
+ }
203
+ for (const shape of entry.shapes) {
204
+ if (!key.startsWith(shape.prefix))
205
+ continue;
206
+ const rest = key.slice(shape.prefix.length);
207
+ if (shape.shape.kind === 'indexedCollection') {
208
+ const dot = rest.indexOf('.');
209
+ if (dot < 0)
210
+ continue;
211
+ const idx = rest.slice(0, dot);
212
+ const sub = rest.slice(dot + 1);
213
+ // Q10: substrate parses the index as `u32`; strings of digits
214
+ // longer than `u32::MAX` (e.g. `"4294967296"`, 2^32) are
215
+ // accepted by the TS regex but rejected by the substrate as
216
+ // `IndexMalformed`. Mirror the u32 range so client-side
217
+ // validation doesn't silently pass payloads the substrate
218
+ // would later reject.
219
+ const idxNum = Number(idx);
220
+ if (!/^\d+$/.test(idx) ||
221
+ !Number.isInteger(idxNum) ||
222
+ idxNum > 0xffff_ffff) {
223
+ errors.push({
224
+ kind: 'index_malformed',
225
+ axis,
226
+ prefix: shape.prefix,
227
+ index: idx,
228
+ tag: tagWire,
229
+ });
230
+ return;
231
+ }
232
+ const subEntry = shape.subKeys.find((e) => e.key === sub);
233
+ if (subEntry) {
234
+ checkValue(subEntry, observedType, observedValue, axis, errors);
235
+ return;
236
+ }
237
+ warnings.push({ kind: 'unknown_key', axis, key });
238
+ return;
239
+ }
240
+ // KeyedMap: the rest IS the user-defined name.
241
+ if (rest.length > 0) {
242
+ const synth = {
243
+ key: shape.prefix,
244
+ valueType: shape.shape.valueType,
245
+ };
246
+ checkValue(synth, observedType, observedValue, axis, errors);
247
+ return;
248
+ }
249
+ }
250
+ warnings.push({ kind: 'unknown_key', axis, key });
251
+ }
252
+ /**
253
+ * Validate a wire-format capability set against a schema. Defaults to
254
+ * the canonical {@link AXIS_SCHEMA}; pass a custom schema for
255
+ * application-specific extensions.
256
+ *
257
+ * Mirrors the substrate's `validate_capabilities`. Output shape pinned
258
+ * by `tests/cross_lang_capability/capability_validation.json`.
259
+ */
260
+ function validateCapabilities(caps, schema = exports.AXIS_SCHEMA) {
261
+ const errors = [];
262
+ const warnings = [];
263
+ for (const wire of caps.tags) {
264
+ const tag = (0, capability_enhancements_1.tagFromString)(wire);
265
+ switch (tag.kind) {
266
+ case 'axisPresent':
267
+ validateAxisKey(tag.axis, tag.key, 'presence', undefined, schema, errors, warnings, wire);
268
+ break;
269
+ case 'axisValue':
270
+ validateAxisKey(tag.axis, tag.key, 'string', tag.value, schema, errors, warnings, wire);
271
+ break;
272
+ case 'reserved':
273
+ // Reserved-prefix tags pass through unchecked.
274
+ break;
275
+ case 'legacy':
276
+ warnings.push({ kind: 'legacy_tag', tag: tag.raw });
277
+ break;
278
+ }
279
+ }
280
+ // P2-H: metadata-key reservation check. The schema declares
281
+ // `metadataReserved` (exact-match) and `metadataReservedPrefixes`
282
+ // (prefix-match) but pre-fix the validator never consulted them
283
+ // — a user's `with_metadata("intent", …)` smuggling onto a
284
+ // scheduler-reserved key emitted no warning. Mirrors the
285
+ // substrate's CR-14 fix.
286
+ for (const key of Object.keys(caps.metadata)) {
287
+ if (schema.metadataReserved.includes(key)) {
288
+ warnings.push({ kind: 'metadata_reserved_key', key });
289
+ continue;
290
+ }
291
+ const prefix = schema.metadataReservedPrefixes.find((p) => key.startsWith(p));
292
+ if (prefix !== undefined) {
293
+ warnings.push({ kind: 'metadata_reserved_prefix', key, prefix });
294
+ }
295
+ }
296
+ // Metadata soft-cap check.
297
+ let metadataBytes = 0;
298
+ for (const [k, v] of Object.entries(caps.metadata)) {
299
+ metadataBytes += k.length + v.length;
300
+ }
301
+ if (metadataBytes > exports.METADATA_SOFT_CAP_BYTES) {
302
+ warnings.push({
303
+ kind: 'metadata_oversize',
304
+ soft_cap_bytes: exports.METADATA_SOFT_CAP_BYTES,
305
+ actual_bytes: metadataBytes,
306
+ });
307
+ }
308
+ return { errors, warnings };
309
+ }
310
+ /** True iff there are zero errors and zero warnings. */
311
+ function isReportClean(r) {
312
+ return r.errors.length === 0 && r.warnings.length === 0;
313
+ }
314
+ /** True iff there are zero errors. Warnings are allowed. */
315
+ function isReportValid(r) {
316
+ return r.errors.length === 0;
317
+ }
@@ -0,0 +1,56 @@
1
+ /**
2
+ * Typed channels — strongly typed pub/sub over named channels.
3
+ */
4
+ import type { Net as NapiNet } from '@net-mesh/core';
5
+ import type { SubscribeOpts } from './types';
6
+ import { EventStream, TypedEventStream } from './stream';
7
+ /**
8
+ * A strongly typed channel for publishing and subscribing to events.
9
+ *
10
+ * @example
11
+ * ```typescript
12
+ * interface TemperatureReading {
13
+ * sensor_id: string;
14
+ * celsius: number;
15
+ * timestamp: number;
16
+ * }
17
+ *
18
+ * const temps = node.channel<TemperatureReading>('sensors/temperature');
19
+ * temps.publish({ sensor_id: 'A1', celsius: 22.5, timestamp: Date.now() });
20
+ *
21
+ * for await (const reading of temps.subscribe()) {
22
+ * console.log(`${reading.sensor_id}: ${reading.celsius}°C`);
23
+ * }
24
+ * ```
25
+ */
26
+ export declare class TypedChannel<T> {
27
+ private bus;
28
+ private channelName;
29
+ private validator?;
30
+ private readonly filter;
31
+ constructor(bus: NapiNet, channelName: string, validator?: (data: unknown) => T);
32
+ /** The channel name. */
33
+ get name(): string;
34
+ /**
35
+ * Publish a typed event to this channel.
36
+ *
37
+ * The event is serialized to JSON with the channel name embedded.
38
+ */
39
+ publish(event: T): boolean;
40
+ /**
41
+ * Publish a batch of typed events to this channel.
42
+ * Returns the number of events successfully published.
43
+ */
44
+ publishBatch(events: T[]): number;
45
+ /**
46
+ * Subscribe to typed events on this channel.
47
+ *
48
+ * Returns an async iterable that deserializes and optionally validates
49
+ * each event.
50
+ */
51
+ subscribe(opts?: SubscribeOpts): TypedEventStream<T>;
52
+ /**
53
+ * Subscribe to raw events on this channel.
54
+ */
55
+ subscribeRaw(opts?: SubscribeOpts): EventStream;
56
+ }
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+ /**
3
+ * Typed channels — strongly typed pub/sub over named channels.
4
+ */
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.TypedChannel = void 0;
7
+ const stream_1 = require("./stream");
8
+ /**
9
+ * A strongly typed channel for publishing and subscribing to events.
10
+ *
11
+ * @example
12
+ * ```typescript
13
+ * interface TemperatureReading {
14
+ * sensor_id: string;
15
+ * celsius: number;
16
+ * timestamp: number;
17
+ * }
18
+ *
19
+ * const temps = node.channel<TemperatureReading>('sensors/temperature');
20
+ * temps.publish({ sensor_id: 'A1', celsius: 22.5, timestamp: Date.now() });
21
+ *
22
+ * for await (const reading of temps.subscribe()) {
23
+ * console.log(`${reading.sensor_id}: ${reading.celsius}°C`);
24
+ * }
25
+ * ```
26
+ */
27
+ class TypedChannel {
28
+ bus;
29
+ channelName;
30
+ validator;
31
+ // Filter is a constant for the lifetime of the channel; build the
32
+ // JSON string once instead of regenerating it on every subscribe /
33
+ // subscribeRaw call.
34
+ filter;
35
+ constructor(bus, channelName, validator) {
36
+ this.bus = bus;
37
+ this.channelName = channelName;
38
+ this.validator = validator;
39
+ this.filter = JSON.stringify({ path: '_channel', value: channelName });
40
+ }
41
+ /** The channel name. */
42
+ get name() {
43
+ return this.channelName;
44
+ }
45
+ /**
46
+ * Publish a typed event to this channel.
47
+ *
48
+ * The event is serialized to JSON with the channel name embedded.
49
+ */
50
+ publish(event) {
51
+ const payload = JSON.stringify({
52
+ ...event,
53
+ _channel: this.channelName,
54
+ });
55
+ return this.bus.ingestFire(payload);
56
+ }
57
+ /**
58
+ * Publish a batch of typed events to this channel.
59
+ * Returns the number of events successfully published.
60
+ */
61
+ publishBatch(events) {
62
+ const payloads = events.map((event) => JSON.stringify({
63
+ ...event,
64
+ _channel: this.channelName,
65
+ }));
66
+ return this.bus.ingestBatchFire(payloads);
67
+ }
68
+ /**
69
+ * Subscribe to typed events on this channel.
70
+ *
71
+ * Returns an async iterable that deserializes and optionally validates
72
+ * each event.
73
+ */
74
+ subscribe(opts = {}) {
75
+ const mergedOpts = {
76
+ ...opts,
77
+ filter: opts.filter ?? this.filter,
78
+ };
79
+ const parse = this.validator
80
+ ? (raw) => this.validator(JSON.parse(raw))
81
+ : (raw) => JSON.parse(raw);
82
+ return new stream_1.TypedEventStream(this.bus, mergedOpts, parse);
83
+ }
84
+ /**
85
+ * Subscribe to raw events on this channel.
86
+ */
87
+ subscribeRaw(opts = {}) {
88
+ const mergedOpts = {
89
+ ...opts,
90
+ filter: opts.filter ?? this.filter,
91
+ };
92
+ return new stream_1.EventStream(this.bus, mergedOpts);
93
+ }
94
+ }
95
+ exports.TypedChannel = TypedChannel;