@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.
Files changed (39) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/dart/CHANGELOG.md +19 -0
  3. package/dart/lib/src/nms_generated_runtime.dart +1 -0
  4. package/dart/lib/src/result_usda.dart +64 -27
  5. package/dart/lib/src/runner/node-entry.mjs +701 -207
  6. package/dart/lib/src/sync_status_projection.dart +11 -0
  7. package/dart/pubspec.lock +1 -1
  8. package/dart/pubspec.yaml +2 -2
  9. package/dart/test/kernel_refused_follow_test.dart +36 -0
  10. package/dart/test/result_usda_test.dart +67 -0
  11. package/dart_types/CHANGELOG.md +6 -0
  12. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +11 -1
  13. package/dart_types/lib/src/vo_canonical.dart +185 -0
  14. package/dart_types/pubspec.lock +1 -1
  15. package/dart_types/pubspec.yaml +3 -1
  16. package/dart_types/test/vo_canonical_test.dart +91 -0
  17. package/package.json +2 -2
  18. package/src/aggregate.ts +3 -1
  19. package/src/baseline_usda.ts +2 -3
  20. package/src/build_package.ts +47 -7
  21. package/src/codegen_call_usda.ts +6 -3
  22. package/src/codegen_dart.ts +19 -57
  23. package/src/codegen_result_usda.ts +8 -4
  24. package/src/compile_package_main.ts +11 -4
  25. package/src/engine_entry.ts +12 -4
  26. package/src/identity_usda.ts +3 -52
  27. package/src/index.ts +10 -0
  28. package/src/intent.ts +28 -3
  29. package/src/law_check.ts +34 -0
  30. package/src/law_graph_usda.ts +2 -3
  31. package/src/law_layer_prims.ts +3 -28
  32. package/src/law_units.ts +7 -6
  33. package/src/law_version.ts +2 -1
  34. package/src/point_guard.ts +5 -1
  35. package/src/release_inspection.ts +6 -4
  36. package/src/release_publication.ts +6 -5
  37. package/src/usda_text.ts +78 -3
  38. package/src/value_identity.ts +355 -0
  39. 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
@@ -222,7 +222,7 @@ packages:
222
222
  path: "../dart_types"
223
223
  relative: true
224
224
  source: path
225
- version: "0.14.0"
225
+ version: "0.15.0"
226
226
  package_config:
227
227
  dependency: "direct main"
228
228
  description:
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.69.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.14.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
  }
@@ -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, 11);
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
+ }
@@ -74,7 +74,7 @@ packages:
74
74
  source: hosted
75
75
  version: "1.15.1"
76
76
  crypto:
77
- dependency: transitive
77
+ dependency: "direct dev"
78
78
  description:
79
79
  name: crypto
80
80
  sha256: c8ea0233063ba03258fbcf2ca4d6dadfefe14f02fab57702265467a19f27fadf
@@ -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.14.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.115.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.115.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
- return JSON.stringify(a) === JSON.stringify(b);
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). */
@@ -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
- function quote(value: string): string {
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
 
@@ -48,7 +48,8 @@ import {
48
48
  applicationLanguageOf,
49
49
  assertApplicationLanguageCoversAuthoredDirectives,
50
50
  executableDirectivesForLanguage,
51
- remapWorkspaceInvariantsForLanguage,
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
- const workspaceInvariants = applicationLanguage === undefined
740
- ? [...workspaceInvariantById.values()]
741
- : remapWorkspaceInvariantsForLanguage(
742
- [...workspaceInvariantById.values()],
743
- applicationLanguage,
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));