@githolon/dsl 0.115.0 → 0.116.4
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.
- package/assets/operation-contract.wasm +0 -0
- package/dart/CHANGELOG.md +19 -0
- package/dart/lib/src/nms_generated_runtime.dart +1 -0
- package/dart/lib/src/result_usda.dart +64 -27
- package/dart/lib/src/runner/node-entry.mjs +701 -207
- package/dart/lib/src/sync_status_projection.dart +11 -0
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/kernel_refused_follow_test.dart +36 -0
- package/dart/test/result_usda_test.dart +67 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +11 -1
- package/dart_types/lib/src/vo_canonical.dart +185 -0
- package/dart_types/pubspec.lock +1 -1
- package/dart_types/pubspec.yaml +3 -1
- package/dart_types/test/vo_canonical_test.dart +91 -0
- package/package.json +2 -2
- package/src/aggregate.ts +3 -1
- package/src/baseline_usda.ts +2 -3
- package/src/build_package.ts +47 -7
- package/src/codegen_call_usda.ts +6 -3
- package/src/codegen_dart.ts +19 -57
- package/src/codegen_result_usda.ts +8 -4
- package/src/compile_package_main.ts +11 -4
- package/src/engine_entry.ts +12 -4
- package/src/identity_usda.ts +3 -52
- package/src/index.ts +10 -0
- package/src/intent.ts +28 -3
- package/src/law_check.ts +34 -0
- package/src/law_graph_usda.ts +2 -3
- package/src/law_layer_prims.ts +3 -28
- package/src/law_units.ts +7 -6
- package/src/law_version.ts +2 -1
- package/src/point_guard.ts +5 -1
- package/src/release_inspection.ts +6 -4
- package/src/release_publication.ts +6 -5
- package/src/usda_text.ts +78 -3
- package/src/value_identity.ts +355 -0
- package/src/value_object.ts +29 -1
|
@@ -126,6 +126,14 @@ enum WorkspaceFollowPhase {
|
|
|
126
126
|
rebornRemotely,
|
|
127
127
|
replacingLocal,
|
|
128
128
|
localCopyUnreadable,
|
|
129
|
+
|
|
130
|
+
/// This peer's kernel deterministically refused the last round at the same
|
|
131
|
+
/// local head and remote frontier: sync is blocked, not retrying. No timer
|
|
132
|
+
/// re-runs it; it runs again on new local work, an upstream head change, a
|
|
133
|
+
/// socket reopen or a changed kernel/runtime. [WorkspaceFollowStatus.retryAt]
|
|
134
|
+
/// is set only once such an event made it due, and
|
|
135
|
+
/// [WorkspaceFollowStatus.detail] carries the kernel's refusal.
|
|
136
|
+
kernelRefused,
|
|
129
137
|
unknown,
|
|
130
138
|
}
|
|
131
139
|
|
|
@@ -182,6 +190,9 @@ final class WorkspaceFollowStatus {
|
|
|
182
190
|
nomos_protocol.FollowObservationPhase
|
|
183
191
|
.FOLLOW_OBSERVATION_PHASE_LOCAL_COPY_UNREADABLE =>
|
|
184
192
|
WorkspaceFollowPhase.localCopyUnreadable,
|
|
193
|
+
nomos_protocol.FollowObservationPhase
|
|
194
|
+
.FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED =>
|
|
195
|
+
WorkspaceFollowPhase.kernelRefused,
|
|
185
196
|
_ => WorkspaceFollowPhase.unknown,
|
|
186
197
|
},
|
|
187
198
|
live: report.live,
|
package/dart/pubspec.lock
CHANGED
package/dart/pubspec.yaml
CHANGED
|
@@ -7,7 +7,7 @@ repository: https://github.com/Captain-App/nomos2
|
|
|
7
7
|
description: >-
|
|
8
8
|
Nomos 2 Dart client — generated domain values, intent payloads and application
|
|
9
9
|
lifecycle types over the typed Nomos kernel protocol and USDA custody model.
|
|
10
|
-
version: 0.
|
|
10
|
+
version: 0.70.2
|
|
11
11
|
|
|
12
12
|
environment:
|
|
13
13
|
sdk: ^3.5.0
|
|
@@ -18,7 +18,7 @@ dependencies:
|
|
|
18
18
|
# The pure-data core (wire types, offer codec, NomosReads, schema validation, error
|
|
19
19
|
# info). nomos_client depends on and re-exports it, so existing imports are unchanged.
|
|
20
20
|
# For local monorepo dev, pubspec_overrides.yaml points this at ../dart_types.
|
|
21
|
-
nomos_types: ^0.
|
|
21
|
+
nomos_types: ^0.15.0
|
|
22
22
|
fixnum: ^1.1.1
|
|
23
23
|
json_schema: ^5.2.2
|
|
24
24
|
crypto: ^3.0.0
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import 'package:fixnum/fixnum.dart' as fixnum;
|
|
2
|
+
import 'package:nomos_client/src/sync_status_projection.dart';
|
|
3
|
+
import 'package:nomos_types/nomos_kernel_protocol.dart' as protocol;
|
|
4
|
+
import 'package:test/test.dart';
|
|
5
|
+
|
|
6
|
+
void main() {
|
|
7
|
+
test('a follow the kernel refused is blocked, not retrying', () {
|
|
8
|
+
final blocked =
|
|
9
|
+
WorkspaceFollowStatus.fromRuntime(protocol.FollowObservation(
|
|
10
|
+
workspace: 'estate',
|
|
11
|
+
phase: protocol
|
|
12
|
+
.FollowObservationPhase.FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED,
|
|
13
|
+
detail: 'NMS-KERNEL-CALL: refused',
|
|
14
|
+
attempt: 2,
|
|
15
|
+
));
|
|
16
|
+
expect(blocked.phase, WorkspaceFollowPhase.kernelRefused);
|
|
17
|
+
expect(blocked.retryAt, isNull,
|
|
18
|
+
reason: 'no timer re-runs a blocked refusal');
|
|
19
|
+
});
|
|
20
|
+
|
|
21
|
+
test('an event made a refused follow due again at its re-run time', () {
|
|
22
|
+
final retryAt = DateTime.utc(2026, 10, 8, 9, 0, 10);
|
|
23
|
+
final status = WorkspaceFollowStatus.fromRuntime(protocol.FollowObservation(
|
|
24
|
+
workspace: 'estate',
|
|
25
|
+
phase: protocol
|
|
26
|
+
.FollowObservationPhase.FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED,
|
|
27
|
+
detail:
|
|
28
|
+
'NMS-KERNEL-CALL: invalid or repeated command frontier commit marker',
|
|
29
|
+
retryAtUnixMillis: fixnum.Int64(retryAt.millisecondsSinceEpoch),
|
|
30
|
+
attempt: 2,
|
|
31
|
+
));
|
|
32
|
+
expect(status.phase, WorkspaceFollowPhase.kernelRefused);
|
|
33
|
+
expect(status.detail, startsWith('NMS-KERNEL-CALL:'));
|
|
34
|
+
expect(status.retryAt, retryAt.toLocal());
|
|
35
|
+
});
|
|
36
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import 'dart:convert';
|
|
2
|
+
import 'dart:io';
|
|
2
3
|
import 'dart:typed_data';
|
|
3
4
|
|
|
4
5
|
import 'package:nomos_client/src/nms_generated_runtime.dart';
|
|
@@ -46,4 +47,70 @@ def NomosReadResult "Result"
|
|
|
46
47
|
'Ship',
|
|
47
48
|
);
|
|
48
49
|
});
|
|
50
|
+
|
|
51
|
+
// CAP-862: the shared conformance vectors. Every canonical literal, and every legacy `\uXXXX`
|
|
52
|
+
// spelling an older kernel wrote, reads back as the value it names.
|
|
53
|
+
test('decodes the canonical and legacy USDA string vectors', () {
|
|
54
|
+
final vectors = jsonDecode(
|
|
55
|
+
File('../conformance/usda_string.v1.json').readAsStringSync())
|
|
56
|
+
as Map<String, dynamic>;
|
|
57
|
+
final cases = [
|
|
58
|
+
...(vectors['encode'] as List).cast<Map<String, dynamic>>(),
|
|
59
|
+
...(vectors['legacyDecode'] as List).cast<Map<String, dynamic>>(),
|
|
60
|
+
];
|
|
61
|
+
expect(cases.length, greaterThan(60));
|
|
62
|
+
for (final vector in cases) {
|
|
63
|
+
final result = NmsUsdaResult.parse(
|
|
64
|
+
Uint8List.fromList(
|
|
65
|
+
utf8.encode('''#usda 1.0
|
|
66
|
+
(
|
|
67
|
+
defaultPrim = "Result"
|
|
68
|
+
)
|
|
69
|
+
|
|
70
|
+
def NomosReadResult "Result"
|
|
71
|
+
{
|
|
72
|
+
custom uniform bool nomos:done = true
|
|
73
|
+
|
|
74
|
+
def NomosListValue "Rows"
|
|
75
|
+
{
|
|
76
|
+
def NomosRecordValue "i_00000000"
|
|
77
|
+
{
|
|
78
|
+
custom uniform string nomos:id = "note:1"
|
|
79
|
+
custom rel nomos:type = </Nomos/Domains/d_1/Aggregates/a_note>
|
|
80
|
+
|
|
81
|
+
def NomosStringValue "f_title"
|
|
82
|
+
{
|
|
83
|
+
custom rel nomos:field = </Nomos/Domains/d_1/Aggregates/a_note/Fields/f_title>
|
|
84
|
+
custom uniform string nomos:value = ${vector['usda']}
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
'''),
|
|
90
|
+
),
|
|
91
|
+
);
|
|
92
|
+
expect(
|
|
93
|
+
result.rows.single
|
|
94
|
+
.requiredField('/Nomos/Domains/d_1/Aggregates/a_note/Fields/f_title')
|
|
95
|
+
.stringValue(),
|
|
96
|
+
vector['value'],
|
|
97
|
+
reason: vector['name'] as String,
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
test('framework-call quoting writes every vector canonically', () {
|
|
103
|
+
final vectors = jsonDecode(
|
|
104
|
+
File('../conformance/usda_string.v1.json').readAsStringSync())
|
|
105
|
+
as Map<String, dynamic>;
|
|
106
|
+
for (final vector
|
|
107
|
+
in (vectors['encode'] as List).cast<Map<String, dynamic>>()) {
|
|
108
|
+
expect(NmsUsdaLayer.quote(vector['value'] as String), vector['usda'],
|
|
109
|
+
reason: vector['name'] as String);
|
|
110
|
+
}
|
|
111
|
+
for (final point in (vectors['refusedCodePoints'] as List).cast<int>()) {
|
|
112
|
+
expect(() => NmsUsdaLayer.quote('a${String.fromCharCode(point)}b'),
|
|
113
|
+
throwsArgumentError);
|
|
114
|
+
}
|
|
115
|
+
});
|
|
49
116
|
}
|
package/dart_types/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,9 @@
|
|
|
1
|
+
## 0.15.0
|
|
2
|
+
|
|
3
|
+
- `FollowObservationPhase.FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED`: this peer's kernel deterministically refused a replica follow's round, so the follow's sync is blocked and is not retrying.
|
|
4
|
+
|
|
5
|
+
- Generated value objects compare by the law's canonical value encoding, `nomos.vo-canonical.v1` (RFC 8785 over the value's `toJson()`): `NmsInternalValueIdentity.equals` / `hash` / `canonical` (CAP-853). Member order never matters, `2` and `2.0` are one number, `-0` is `0`, and an omitted optional member differs from `null`.
|
|
6
|
+
|
|
1
7
|
## 0.14.0
|
|
2
8
|
|
|
3
9
|
- Live stream frames for peer catch-up (CAP-826): `LiveOfferFrame` gains checkpoint, history, ref, pack and
|
|
@@ -196,6 +196,15 @@ class FollowObservationPhase extends $pb.ProtobufEnum {
|
|
|
196
196
|
? ''
|
|
197
197
|
: 'FOLLOW_OBSERVATION_PHASE_LOCAL_COPY_UNREADABLE');
|
|
198
198
|
|
|
199
|
+
/// This peer's kernel deterministically refused the follow's last round at this local head and
|
|
200
|
+
/// remote frontier. Sync is BLOCKED: no timer re-runs the same refused round. Only an event
|
|
201
|
+
/// (new local work, an upstream head change, a socket reopen, a changed kernel or runtime) runs
|
|
202
|
+
/// it again; `retry_at_unix_millis` is present only once an event made it due. `detail` carries
|
|
203
|
+
/// the kernel's refusal.
|
|
204
|
+
static const FollowObservationPhase FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED =
|
|
205
|
+
FollowObservationPhase._(
|
|
206
|
+
12, _omitEnumNames ? '' : 'FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED');
|
|
207
|
+
|
|
199
208
|
static const $core.List<FollowObservationPhase> values =
|
|
200
209
|
<FollowObservationPhase>[
|
|
201
210
|
FOLLOW_OBSERVATION_PHASE_UNSPECIFIED,
|
|
@@ -210,10 +219,11 @@ class FollowObservationPhase extends $pb.ProtobufEnum {
|
|
|
210
219
|
FOLLOW_OBSERVATION_PHASE_REBORN_REMOTELY,
|
|
211
220
|
FOLLOW_OBSERVATION_PHASE_REPLACING_LOCAL,
|
|
212
221
|
FOLLOW_OBSERVATION_PHASE_LOCAL_COPY_UNREADABLE,
|
|
222
|
+
FOLLOW_OBSERVATION_PHASE_KERNEL_REFUSED,
|
|
213
223
|
];
|
|
214
224
|
|
|
215
225
|
static final $core.List<FollowObservationPhase?> _byValue =
|
|
216
|
-
$pb.ProtobufEnum.$_initByValueList(values,
|
|
226
|
+
$pb.ProtobufEnum.$_initByValueList(values, 12);
|
|
217
227
|
static FollowObservationPhase? valueOf($core.int value) =>
|
|
218
228
|
value < 0 || value >= _byValue.length ? null : _byValue[value];
|
|
219
229
|
|
|
@@ -0,0 +1,185 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth.
|
|
3
|
+
|
|
4
|
+
/// Value identity for generated value objects — the Dart side of `nomos.vo-canonical.v1` (CAP-853).
|
|
5
|
+
///
|
|
6
|
+
/// The law's definition lives in `dsl/src/value_identity.ts`: RFC 8785 (JCS) over the value the
|
|
7
|
+
/// value object's descriptor admits. A generated class's `toJson()` already IS that typed value
|
|
8
|
+
/// (absent optionals omitted, nullable members `null`, the variant case in `type`), so the Dart side
|
|
9
|
+
/// is the RFC 8785 encoder over it — members in UTF-16 code-unit order, ECMAScript number spelling,
|
|
10
|
+
/// RFC 8785 string escapes — and generated `==` / `hashCode` compare that encoding. One definition
|
|
11
|
+
/// across runtimes: the conformance vectors in `dsl/conformance/vo_canonical.v1.json` are replayed
|
|
12
|
+
/// here (`test/vo_canonical_test.dart`), by the TypeScript suite and by the sealed engine.
|
|
13
|
+
///
|
|
14
|
+
/// Framework-internal: generated code calls [NmsInternalValueIdentity]; it is not exported from the
|
|
15
|
+
/// `nomos_types` barrel. Digests are the law's to derive and store.
|
|
16
|
+
library;
|
|
17
|
+
|
|
18
|
+
const String nmsVoCanonicalEncoding = 'nomos.vo-canonical.v1';
|
|
19
|
+
|
|
20
|
+
/// The value-identity operations generated value objects use for `==` and `hashCode`.
|
|
21
|
+
abstract final class NmsInternalValueIdentity {
|
|
22
|
+
/// The `nomos.vo-canonical.v1` text of a value object's json (`toJson()`).
|
|
23
|
+
///
|
|
24
|
+
/// Throws [ArgumentError] for what the law refuses: NaN or Infinity, an integer outside the
|
|
25
|
+
/// exact JavaScript range, a string that is not well-formed UTF-16, or a non-json value.
|
|
26
|
+
static String canonical(Object? json) {
|
|
27
|
+
final out = StringBuffer();
|
|
28
|
+
_write(out, json, strict: true, at: 'value');
|
|
29
|
+
return out.toString();
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/// Value equality of two value objects' json over the canonical encoding.
|
|
33
|
+
///
|
|
34
|
+
/// Total: a value the law would refuse (NaN, a lone surrogate) still compares, by a spelling
|
|
35
|
+
/// that never collides with an admissible value's canonical text.
|
|
36
|
+
static bool equals(Object? a, Object? b) => _lenient(a) == _lenient(b);
|
|
37
|
+
|
|
38
|
+
/// A hash consistent with [equals].
|
|
39
|
+
static int hash(Object? json) => _lenient(json).hashCode;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
String _lenient(Object? json) {
|
|
43
|
+
final out = StringBuffer();
|
|
44
|
+
_write(out, json, strict: false, at: 'value');
|
|
45
|
+
return out.toString();
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
void _write(StringBuffer out, Object? v,
|
|
49
|
+
{required bool strict, required String at}) {
|
|
50
|
+
if (v == null) {
|
|
51
|
+
out.write('null');
|
|
52
|
+
} else if (v is bool) {
|
|
53
|
+
out.write(v ? 'true' : 'false');
|
|
54
|
+
} else if (v is num) {
|
|
55
|
+
out.write(_number(v, strict: strict, at: at));
|
|
56
|
+
} else if (v is String) {
|
|
57
|
+
_string(out, v, strict: strict, at: at);
|
|
58
|
+
} else if (v is List) {
|
|
59
|
+
out.write('[');
|
|
60
|
+
for (var i = 0; i < v.length; i++) {
|
|
61
|
+
if (i > 0) out.write(',');
|
|
62
|
+
_write(out, v[i], strict: strict, at: '$at[$i]');
|
|
63
|
+
}
|
|
64
|
+
out.write(']');
|
|
65
|
+
} else if (v is Map) {
|
|
66
|
+
final keys = v.keys.map((k) {
|
|
67
|
+
if (k is! String)
|
|
68
|
+
throw ArgumentError(
|
|
69
|
+
'$nmsVoCanonicalEncoding: $at has a non-string member name');
|
|
70
|
+
return k;
|
|
71
|
+
}).toList()
|
|
72
|
+
// String.compareTo orders by UTF-16 code units, as RFC 8785 §3.2.3 requires.
|
|
73
|
+
..sort();
|
|
74
|
+
out.write('{');
|
|
75
|
+
var first = true;
|
|
76
|
+
for (final k in keys) {
|
|
77
|
+
if (!first) out.write(',');
|
|
78
|
+
first = false;
|
|
79
|
+
_string(out, k, strict: strict, at: '$at member name');
|
|
80
|
+
out.write(':');
|
|
81
|
+
_write(out, v[k], strict: strict, at: '$at.$k');
|
|
82
|
+
}
|
|
83
|
+
out.write('}');
|
|
84
|
+
} else {
|
|
85
|
+
throw ArgumentError(
|
|
86
|
+
'$nmsVoCanonicalEncoding: $at is a ${v.runtimeType}, which json cannot carry');
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
const int _maxSafeInteger = 9007199254740991;
|
|
91
|
+
|
|
92
|
+
/// On the web every number is a JavaScript double, so an integral double also `is int`.
|
|
93
|
+
const bool _numbersAreDoubles = identical(1, 1.0);
|
|
94
|
+
|
|
95
|
+
/// ECMAScript Number::toString — the RFC 8785 number spelling — for a Dart [num].
|
|
96
|
+
String _number(num v, {required bool strict, required String at}) {
|
|
97
|
+
if (v is int && !(_numbersAreDoubles && v.abs() > _maxSafeInteger)) {
|
|
98
|
+
if (v.abs() > _maxSafeInteger) {
|
|
99
|
+
if (strict) {
|
|
100
|
+
throw ArgumentError(
|
|
101
|
+
'$nmsVoCanonicalEncoding: $at is an integer outside the exact JavaScript range');
|
|
102
|
+
}
|
|
103
|
+
return 'int:$v';
|
|
104
|
+
}
|
|
105
|
+
return v.toString();
|
|
106
|
+
}
|
|
107
|
+
final d = v.toDouble();
|
|
108
|
+
if (d.isNaN || d.isInfinite) {
|
|
109
|
+
if (strict) {
|
|
110
|
+
throw ArgumentError(
|
|
111
|
+
'$nmsVoCanonicalEncoding: $at is not a finite number (NaN and Infinity have no canonical form)');
|
|
112
|
+
}
|
|
113
|
+
return 'nonfinite:$d';
|
|
114
|
+
}
|
|
115
|
+
if (d == 0) return '0'; // -0 too
|
|
116
|
+
if (d < 0) return '-${_number(-d, strict: strict, at: at)}';
|
|
117
|
+
// The shortest round-trip digits and the exponent, from Dart's own shortest representation.
|
|
118
|
+
final exponential = d.toStringAsExponential(); // e.g. "1.2345e+2", "5e-324"
|
|
119
|
+
final eIndex = exponential.indexOf('e');
|
|
120
|
+
final digits = exponential.substring(0, eIndex).replaceAll('.', '');
|
|
121
|
+
final k = digits.length;
|
|
122
|
+
final n = int.parse(exponential.substring(eIndex + 1)) +
|
|
123
|
+
1; // value = 0.digits × 10^n
|
|
124
|
+
if (k <= n && n <= 21) return digits + '0' * (n - k);
|
|
125
|
+
if (0 < n && n <= 21)
|
|
126
|
+
return '${digits.substring(0, n)}.${digits.substring(n)}';
|
|
127
|
+
if (-6 < n && n <= 0) return '0.${'0' * -n}$digits';
|
|
128
|
+
final e = n - 1;
|
|
129
|
+
final exponent = e < 0 ? '-${-e}' : '+$e';
|
|
130
|
+
return k == 1
|
|
131
|
+
? '${digits}e$exponent'
|
|
132
|
+
: '${digits[0]}.${digits.substring(1)}e$exponent';
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
void _string(StringBuffer out, String s,
|
|
136
|
+
{required bool strict, required String at}) {
|
|
137
|
+
out.write('"');
|
|
138
|
+
for (var i = 0; i < s.length; i++) {
|
|
139
|
+
final c = s.codeUnitAt(i);
|
|
140
|
+
if (c >= 0xd800 && c <= 0xdbff) {
|
|
141
|
+
final next = i + 1 < s.length ? s.codeUnitAt(i + 1) : -1;
|
|
142
|
+
if (next >= 0xdc00 && next <= 0xdfff) {
|
|
143
|
+
out.writeCharCode(c);
|
|
144
|
+
out.writeCharCode(next);
|
|
145
|
+
i++;
|
|
146
|
+
continue;
|
|
147
|
+
}
|
|
148
|
+
if (strict)
|
|
149
|
+
throw ArgumentError(
|
|
150
|
+
'$nmsVoCanonicalEncoding: $at is not well-formed UTF-16 (a lone high surrogate)');
|
|
151
|
+
out.write('\\u${c.toRadixString(16)}');
|
|
152
|
+
continue;
|
|
153
|
+
}
|
|
154
|
+
if (c >= 0xdc00 && c <= 0xdfff) {
|
|
155
|
+
if (strict)
|
|
156
|
+
throw ArgumentError(
|
|
157
|
+
'$nmsVoCanonicalEncoding: $at is not well-formed UTF-16 (a lone low surrogate)');
|
|
158
|
+
out.write('\\u${c.toRadixString(16)}');
|
|
159
|
+
continue;
|
|
160
|
+
}
|
|
161
|
+
switch (c) {
|
|
162
|
+
case 0x22:
|
|
163
|
+
out.write(r'\"');
|
|
164
|
+
case 0x5c:
|
|
165
|
+
out.write(r'\\');
|
|
166
|
+
case 0x08:
|
|
167
|
+
out.write(r'\b');
|
|
168
|
+
case 0x0c:
|
|
169
|
+
out.write(r'\f');
|
|
170
|
+
case 0x0a:
|
|
171
|
+
out.write(r'\n');
|
|
172
|
+
case 0x0d:
|
|
173
|
+
out.write(r'\r');
|
|
174
|
+
case 0x09:
|
|
175
|
+
out.write(r'\t');
|
|
176
|
+
default:
|
|
177
|
+
if (c < 0x20) {
|
|
178
|
+
out.write('\\u00${c.toRadixString(16).padLeft(2, '0')}');
|
|
179
|
+
} else {
|
|
180
|
+
out.writeCharCode(c);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
out.write('"');
|
|
185
|
+
}
|
package/dart_types/pubspec.lock
CHANGED
package/dart_types/pubspec.yaml
CHANGED
|
@@ -9,7 +9,7 @@ description: >-
|
|
|
9
9
|
Pure kernel wire/data types for Nomos Dart frontends — no transport, no bridge,
|
|
10
10
|
no runtime. Typed protobuf and USDA boundary values plus the offer codec,
|
|
11
11
|
reactive read engine, schema validation, and data-only error/trace types.
|
|
12
|
-
version: 0.
|
|
12
|
+
version: 0.15.0
|
|
13
13
|
|
|
14
14
|
environment:
|
|
15
15
|
sdk: ^3.5.0
|
|
@@ -20,4 +20,6 @@ dependencies:
|
|
|
20
20
|
fixnum: ^1.1.1
|
|
21
21
|
|
|
22
22
|
dev_dependencies:
|
|
23
|
+
# The value-identity conformance test checks vector digests (test/vo_canonical_test.dart).
|
|
24
|
+
crypto: ^3.0.0
|
|
23
25
|
test: ^1.25.0
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
// `nomos.vo-canonical.v1` (CAP-853): the Dart encoder replays the law's conformance vectors —
|
|
2
|
+
// dsl/conformance/vo_canonical.v1.json, shared with the TypeScript suite and the sealed engine.
|
|
3
|
+
import 'dart:convert';
|
|
4
|
+
import 'dart:io';
|
|
5
|
+
import 'dart:typed_data';
|
|
6
|
+
|
|
7
|
+
import 'package:crypto/crypto.dart' show sha256;
|
|
8
|
+
import 'package:nomos_types/src/vo_canonical.dart';
|
|
9
|
+
import 'package:test/test.dart';
|
|
10
|
+
|
|
11
|
+
void main() {
|
|
12
|
+
final file =
|
|
13
|
+
jsonDecode(File('../conformance/vo_canonical.v1.json').readAsStringSync())
|
|
14
|
+
as Map<String, Object?>;
|
|
15
|
+
|
|
16
|
+
test('names the same encoding', () {
|
|
17
|
+
expect(file['encoding'], nmsVoCanonicalEncoding);
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
test('spells every RFC 8785 Appendix B number exactly', () {
|
|
21
|
+
for (final n in (file['numbers']! as List).cast<Map<String, Object?>>()) {
|
|
22
|
+
final bits = BigInt.parse(n['bits']! as String, radix: 16);
|
|
23
|
+
final bytes = ByteData(8)
|
|
24
|
+
..setUint32(0, (bits >> 32).toInt())
|
|
25
|
+
..setUint32(4, (bits & BigInt.from(0xffffffff)).toInt());
|
|
26
|
+
expect(NmsInternalValueIdentity.canonical(bytes.getFloat64(0)),
|
|
27
|
+
n['canonical'],
|
|
28
|
+
reason: n['bits'] as String);
|
|
29
|
+
}
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
test('encodes and digests every vector', () {
|
|
33
|
+
for (final v in (file['vectors']! as List).cast<Map<String, Object?>>()) {
|
|
34
|
+
expect(NmsInternalValueIdentity.canonical(v['value']), v['canonical'],
|
|
35
|
+
reason: v['name'] as String);
|
|
36
|
+
expect(
|
|
37
|
+
sha256
|
|
38
|
+
.convert(
|
|
39
|
+
utf8.encode(NmsInternalValueIdentity.canonical(v['value'])))
|
|
40
|
+
.toString(),
|
|
41
|
+
v['sha256'],
|
|
42
|
+
reason: v['name'] as String);
|
|
43
|
+
}
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
test('judges equality like the law', () {
|
|
47
|
+
for (final e
|
|
48
|
+
in (file['equalities']! as List).cast<Map<String, Object?>>()) {
|
|
49
|
+
expect(NmsInternalValueIdentity.equals(e['a'], e['b']), e['equal'],
|
|
50
|
+
reason: e['name'] as String);
|
|
51
|
+
if (e['equal'] == true)
|
|
52
|
+
expect(NmsInternalValueIdentity.hash(e['a']),
|
|
53
|
+
NmsInternalValueIdentity.hash(e['b']),
|
|
54
|
+
reason: e['name'] as String);
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
test('refuses what the law refuses without a descriptor', () {
|
|
59
|
+
for (final r in (file['refusals']! as List)
|
|
60
|
+
.cast<Map<String, Object?>>()
|
|
61
|
+
.where((r) => r['generic'] == true)) {
|
|
62
|
+
expect(() => NmsInternalValueIdentity.canonical(r['value']),
|
|
63
|
+
throwsArgumentError,
|
|
64
|
+
reason: r['name'] as String);
|
|
65
|
+
// Equality stays total: a refused value still compares, and never equals an admissible one.
|
|
66
|
+
expect(NmsInternalValueIdentity.equals(r['value'], r['value']), isTrue);
|
|
67
|
+
}
|
|
68
|
+
for (final bad in [
|
|
69
|
+
double.nan,
|
|
70
|
+
double.infinity,
|
|
71
|
+
double.negativeInfinity,
|
|
72
|
+
9007199254740992
|
|
73
|
+
]) {
|
|
74
|
+
expect(() => NmsInternalValueIdentity.canonical({'x': bad}),
|
|
75
|
+
throwsArgumentError,
|
|
76
|
+
reason: '$bad');
|
|
77
|
+
}
|
|
78
|
+
expect(
|
|
79
|
+
NmsInternalValueIdentity.equals({'x': double.nan}, {'x': double.nan}),
|
|
80
|
+
isTrue);
|
|
81
|
+
});
|
|
82
|
+
|
|
83
|
+
test('a double read back integral is the same value as the authored int', () {
|
|
84
|
+
expect(
|
|
85
|
+
NmsInternalValueIdentity.equals(
|
|
86
|
+
{'x': 2, 'y': -0.0}, {'y': 0, 'x': 2.0}),
|
|
87
|
+
isTrue);
|
|
88
|
+
expect(NmsInternalValueIdentity.hash({'x': 2, 'y': -0.0}),
|
|
89
|
+
NmsInternalValueIdentity.hash({'y': 0, 'x': 2.0}));
|
|
90
|
+
});
|
|
91
|
+
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.116.4",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"sideEffects": false,
|
|
6
6
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
@@ -77,7 +77,7 @@
|
|
|
77
77
|
"test": "vitest run"
|
|
78
78
|
},
|
|
79
79
|
"dependencies": {
|
|
80
|
-
"@githolon/runtime": "^0.
|
|
80
|
+
"@githolon/runtime": "^0.116.4",
|
|
81
81
|
"@bufbuild/protobuf": "^2.13.0",
|
|
82
82
|
"@bufbuild/protoc-gen-es": "^2.13.0",
|
|
83
83
|
"esbuild": "0.24.2",
|
package/src/aggregate.ts
CHANGED
|
@@ -18,6 +18,7 @@ import { admitsNull, attributionField, t } from "./fields.js";
|
|
|
18
18
|
import { satisfiesContract, type ContractHandle } from "./contract.js";
|
|
19
19
|
import type { FulfilmentDecl } from "./fulfilment.js";
|
|
20
20
|
import { Lww } from "./drivers.js";
|
|
21
|
+
import { canonicalJsonValue } from "./value_identity.js";
|
|
21
22
|
|
|
22
23
|
/**
|
|
23
24
|
* A pure, replay-stable predicate over one aggregate's PROJECTED snapshot —
|
|
@@ -99,7 +100,8 @@ export function birthKeyWriteOnceFields(aggregate: AggregateHandle): readonly st
|
|
|
99
100
|
function sameKeyValue(a: unknown, b: unknown): boolean {
|
|
100
101
|
if (a === b) return true;
|
|
101
102
|
if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) return false;
|
|
102
|
-
|
|
103
|
+
// Value equality (RFC 8785), not text equality: member order is not part of a key's value (CAP-853).
|
|
104
|
+
return canonicalJsonValue(a) === canonicalJsonValue(b);
|
|
103
105
|
}
|
|
104
106
|
|
|
105
107
|
/** @internal The write-once judgement over one transition (exported for direct tests). */
|
package/src/baseline_usda.ts
CHANGED
|
@@ -14,6 +14,7 @@
|
|
|
14
14
|
// Typed USDA, like everything Nomos remembers: the identity scopes are the identity layer's own
|
|
15
15
|
// encoding (identity_usda.ts `renderIdentityScopes`), hosted under `NomosBaseline` beside the
|
|
16
16
|
// provenance and wire-allocation scopes this artifact adds.
|
|
17
|
+
import { usdaString } from "./usda_text.js";
|
|
17
18
|
import { createHash } from "node:crypto";
|
|
18
19
|
import type { CompilerIdentityModel } from "./identity_model.js";
|
|
19
20
|
import {
|
|
@@ -60,9 +61,7 @@ export interface ReleasedBaseline {
|
|
|
60
61
|
readonly wireAllocations: readonly BaselineWireAllocation[];
|
|
61
62
|
}
|
|
62
63
|
|
|
63
|
-
|
|
64
|
-
return JSON.stringify(value);
|
|
65
|
-
}
|
|
64
|
+
const quote = usdaString;
|
|
66
65
|
const stringList = (items: readonly string[]): string => `[${items.map(quote).join(", ")}]`;
|
|
67
66
|
const byteOrder = (left: string, right: string): number => (left < right ? -1 : left > right ? 1 : 0);
|
|
68
67
|
|
package/src/build_package.ts
CHANGED
|
@@ -48,7 +48,8 @@ import {
|
|
|
48
48
|
applicationLanguageOf,
|
|
49
49
|
assertApplicationLanguageCoversAuthoredDirectives,
|
|
50
50
|
executableDirectivesForLanguage,
|
|
51
|
-
|
|
51
|
+
carryWorkspaceInvariantsToPerformingIntents,
|
|
52
|
+
isBusinessIntent,
|
|
52
53
|
} from "./intent.js";
|
|
53
54
|
import { synthesizePreconditionInvariants } from "./precondition.js";
|
|
54
55
|
import {
|
|
@@ -229,6 +230,39 @@ export interface DomainModuleSpec {
|
|
|
229
230
|
* the read-models, and apply the HASH-LOAD-BEARING omit-when-empty key discipline
|
|
230
231
|
* (an empty optional key must be ABSENT or every domain's canonical manifest hash drifts).
|
|
231
232
|
*/
|
|
233
|
+
/**
|
|
234
|
+
* REFUSE A DANGLING INVARIANT (CAP-863). The kernel runs a workspace invariant only for a dispatch whose
|
|
235
|
+
* directive id equals its `.on(...)` target, and an effect's invariant reaches an intent only by being
|
|
236
|
+
* carried onto it. An invariant whose target this domain neither dispatches nor performs from a routable
|
|
237
|
+
* intent would compile, hash into the law and never run: a rule the author believes is enforced and is
|
|
238
|
+
* not. (The law graph later refuses a target outside the domain's intent closure as NMS-GRAPH-001; this
|
|
239
|
+
* check runs first, names every dangling invariant, and also covers a private effect no intent performs.)
|
|
240
|
+
*/
|
|
241
|
+
export function assertWorkspaceInvariantTargetsDispatchable(
|
|
242
|
+
invariants: readonly WorkspaceInvariantDecl[],
|
|
243
|
+
dispatchable: readonly Directive<any>[],
|
|
244
|
+
domain: string,
|
|
245
|
+
): void {
|
|
246
|
+
const judged = new Set(dispatchable.map((directive) => directive.id));
|
|
247
|
+
// An effect a routable intent performs is judged through that intent's carried copy.
|
|
248
|
+
for (const directive of dispatchable) {
|
|
249
|
+
if (isBusinessIntent(directive)) {
|
|
250
|
+
for (const step of directive.steps) judged.add(step.directive.id);
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
const dangling = invariants
|
|
254
|
+
.filter((invariant) => !judged.has(invariant.on))
|
|
255
|
+
.map((invariant) => `'${invariant.id}' on '${invariant.on}'`)
|
|
256
|
+
.sort();
|
|
257
|
+
if (dangling.length > 0) {
|
|
258
|
+
throw new Error(
|
|
259
|
+
`domain '${domain}' declares workspace invariant ${dangling.join(", ")}, but nothing in the compiled ` +
|
|
260
|
+
`domain dispatches that target or performs it from an intent, so the invariant could never run. ` +
|
|
261
|
+
`Export the directive, or perform it from an intent the domain publishes.`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
|
|
232
266
|
export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
233
267
|
const domain = spec.domain ?? spec.name;
|
|
234
268
|
const indexedVariants = indexedVariantsOf(spec.modules);
|
|
@@ -736,12 +770,17 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
736
770
|
for (const invariant of spec.workspaceInvariants ?? []) {
|
|
737
771
|
workspaceInvariantById.set(invariant.id, invariant);
|
|
738
772
|
}
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
773
|
+
// CAP-863: one carry rule with or without `language(...)` — an effect's invariant judges every
|
|
774
|
+
// routable intent that performs it. Without a language the original declaration is always kept, exactly
|
|
775
|
+
// as before: its bare id may also be dispatched directly or by another installed law, so carrying only
|
|
776
|
+
// ever ADDS judgements. A language closes its private effects, so (as before) only a still-dispatchable
|
|
777
|
+
// effect keeps its original.
|
|
778
|
+
const dispatchableDirectiveIds = new Set(directives.map((directive) => directive.id));
|
|
779
|
+
const workspaceInvariants = carryWorkspaceInvariantsToPerformingIntents(
|
|
780
|
+
[...workspaceInvariantById.values()],
|
|
781
|
+
directives.filter(isBusinessIntent),
|
|
782
|
+
(directiveId) => applicationLanguage === undefined || dispatchableDirectiveIds.has(directiveId),
|
|
783
|
+
);
|
|
745
784
|
for (const policy of applicationLanguage?.workspaceSharing ?? []) {
|
|
746
785
|
if (!workspaceInvariants.some((candidate) => candidate.id === policy.invitationLaw.gate.id)) {
|
|
747
786
|
workspaceInvariants.push(policy.invitationLaw.gate);
|
|
@@ -769,6 +808,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
769
808
|
// the same desugaring lane as preconditions, so the manifest (`nomosInvariantGate`) and the sealed
|
|
770
809
|
// engine (which synthesizes from the SAME directives in `buildDomainSlice`) can never disagree.
|
|
771
810
|
for (const d of directives) workspaceInvariants.push(...synthesizeBirthQuotaInvariants(d));
|
|
811
|
+
assertWorkspaceInvariantTargetsDispatchable(workspaceInvariants, directives, domain);
|
|
772
812
|
// The meter's declared queries route like any exported query (read manifest + captured-read re-derive).
|
|
773
813
|
if (declaresBirth) {
|
|
774
814
|
const haveQuery = new Set(queries.map((q) => q.id));
|