@githolon/dsl 0.115.0 → 0.116.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.
Files changed (39) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/dart/CHANGELOG.md +15 -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
@@ -200,7 +200,8 @@ export const TS_CALL_USDA_PRELUDE = [
200
200
  ` else if (character === "\\t") out += "\\\\t";`,
201
201
  ` else if (character === "\\b") out += "\\\\b";`,
202
202
  ` else if (character === "\\f") out += "\\\\f";`,
203
- ` else if (point < 0x20 || (point >= 0x7f && point <= 0x9f)) out += "\\\\u" + point.toString(16).padStart(4, "0");`,
203
+ ` else if (point < 0x20 || point === 0x7f) out += "\\\\x" + point.toString(16).padStart(2, "0");`,
204
+ ` else if (point >= 0xd800 && point <= 0xdfff) throw new TypeError("Nomos USDA strings cannot carry a lone surrogate");`,
204
205
  ` else out += character;`,
205
206
  ` }`,
206
207
  ` return out + "\\\"";`,
@@ -254,8 +255,10 @@ export const DART_CALL_USDA_PRELUDE = [
254
255
  ` case 0x08: out.write(r'\\b');`,
255
256
  ` case 0x0c: out.write(r'\\f');`,
256
257
  ` default:`,
257
- ` if (point < 0x20 || (point >= 0x7f && point <= 0x9f)) {`,
258
- ` out.write('\\\\u\${point.toRadixString(16).padLeft(4, '0')}');`,
258
+ ` if (point < 0x20 || point == 0x7f) {`,
259
+ ` out.write('\\\\x\${point.toRadixString(16).padLeft(2, '0')}');`,
260
+ ` } else if (point >= 0xd800 && point <= 0xdfff) {`,
261
+ ` throw ArgumentError.value(value, 'value', 'Nomos USDA strings cannot carry a lone surrogate');`,
259
262
  ` } else {`,
260
263
  ` out.write(String.fromCharCode(point));`,
261
264
  ` }`,
@@ -2133,54 +2133,16 @@ function voToJsonExpr(f: ValueObjectFieldDescriptor, expr: string, depth = 0): s
2133
2133
  }
2134
2134
  }
2135
2135
 
2136
- /** Deep json equality/hash helpers shared by every generated VO class — emitted ONCE
2137
- * per file when the file emits ≥1 VO class (`toJson()`-based value equality). */
2138
- function emitVoHelpers(includeCopyWithSentinel: boolean): string {
2136
+ /** File-level helpers for generated VO classes — the copyWith absent-vs-null sentinel, emitted ONCE
2137
+ * per file when a VO class needs it. Value equality is NOT emitted here: every VO's `==` / `hashCode`
2138
+ * call `NmsInternalValueIdentity` (nomos_types), the Dart implementation of the law's
2139
+ * `nomos.vo-canonical.v1` encoding (CAP-853) — one definition across runtimes. */
2140
+ function emitVoHelpers(): string {
2139
2141
  return [
2140
- ...(includeCopyWithSentinel
2141
- ? [
2142
- `/// The copyWith ABSENT-vs-null sentinel: an omitted optional param keeps the current`,
2143
- `/// value; an explicit \`null\` clears it. (Dart cannot distinguish the two otherwise.)`,
2144
- `const Object _voUnset = Object();`,
2145
- ``,
2146
- ]
2147
- : []),
2148
- `/// Deep json equality for generated value-object classes (toJson-based).`,
2149
- `bool _voJsonEq(Object? a, Object? b) {`,
2150
- ` if (a is Map && b is Map) {`,
2151
- ` if (a.length != b.length) {`,
2152
- ` return false;`,
2153
- ` }`,
2154
- ` for (final k in a.keys) {`,
2155
- ` if (!b.containsKey(k) || !_voJsonEq(a[k], b[k])) {`,
2156
- ` return false;`,
2157
- ` }`,
2158
- ` }`,
2159
- ` return true;`,
2160
- ` }`,
2161
- ` if (a is List && b is List) {`,
2162
- ` if (a.length != b.length) {`,
2163
- ` return false;`,
2164
- ` }`,
2165
- ` for (var i = 0; i < a.length; i++) {`,
2166
- ` if (!_voJsonEq(a[i], b[i])) {`,
2167
- ` return false;`,
2168
- ` }`,
2169
- ` }`,
2170
- ` return true;`,
2171
- ` }`,
2172
- ` return a == b;`,
2173
- `}`,
2142
+ `/// The copyWith ABSENT-vs-null sentinel: an omitted optional param keeps the current`,
2143
+ `/// value; an explicit \`null\` clears it. (Dart cannot distinguish the two otherwise.)`,
2144
+ `const Object _voUnset = Object();`,
2174
2145
  ``,
2175
- `/// Deep json hash for generated value-object classes (toJson-based).`,
2176
- `int _voJsonHash(Object? v) {`,
2177
- ` if (v is Map) {`,
2178
- ` return Object.hashAllUnordered(`,
2179
- ` v.entries.map((e) => Object.hash(e.key, _voJsonHash(e.value))));`,
2180
- ` }`,
2181
- ` if (v is List) return Object.hashAll(v.map(_voJsonHash));`,
2182
- ` return v.hashCode;`,
2183
- `}`,
2184
2146
  ].join("\n");
2185
2147
  }
2186
2148
 
@@ -2261,9 +2223,9 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
2261
2223
  ` /// A copy with [value] replaced.`,
2262
2224
  ` ${cls} copyWith({${arm.ref}? value}) => ${cls}(value ?? this.value);`,
2263
2225
  ` @override`,
2264
- ` bool operator ==(Object other) => other is ${cls} && _voJsonEq(other.toJson(), toJson());`,
2226
+ ` bool operator ==(Object other) => other is ${cls} && NmsInternalValueIdentity.equals(other.toJson(), toJson());`,
2265
2227
  ` @override`,
2266
- ` int get hashCode => _voJsonHash(toJson());`,
2228
+ ` int get hashCode => NmsInternalValueIdentity.hash(toJson());`,
2267
2229
  `}`,
2268
2230
  ].join("\n");
2269
2231
  }
@@ -2313,9 +2275,9 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
2313
2275
  ``,
2314
2276
  ...emitCopyWith(cls, fields.map((x) => ({ member: x.member, dartType: dartTypeOfVoField(x.f, cls, x.wireName), optional: x.optional || x.f.nullable === true }))),
2315
2277
  ` @override`,
2316
- ` bool operator ==(Object other) => other is ${cls} && _voJsonEq(other.toJson(), toJson());`,
2278
+ ` bool operator ==(Object other) => other is ${cls} && NmsInternalValueIdentity.equals(other.toJson(), toJson());`,
2317
2279
  ` @override`,
2318
- ` int get hashCode => _voJsonHash(toJson());`,
2280
+ ` int get hashCode => NmsInternalValueIdentity.hash(toJson());`,
2319
2281
  `}`,
2320
2282
  ].join("\n");
2321
2283
  });
@@ -2332,9 +2294,9 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
2332
2294
  ` @override`,
2333
2295
  ` Map<String, Object?> toJson() => raw;`,
2334
2296
  ` @override`,
2335
- ` bool operator ==(Object other) => other is ${d.name}Unknown && _voJsonEq(other.raw, raw);`,
2297
+ ` bool operator ==(Object other) => other is ${d.name}Unknown && NmsInternalValueIdentity.equals(other.raw, raw);`,
2336
2298
  ` @override`,
2337
- ` int get hashCode => _voJsonHash(raw);`,
2299
+ ` int get hashCode => NmsInternalValueIdentity.hash(raw);`,
2338
2300
  `}`,
2339
2301
  ].join("\n");
2340
2302
 
@@ -2432,10 +2394,10 @@ function emitValueObjectClass(d: ValueObjectDescriptor): string {
2432
2394
  ``,
2433
2395
  ` @override`,
2434
2396
  ` bool operator ==(Object other) =>`,
2435
- ` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
2397
+ ` other is ${d.name} && NmsInternalValueIdentity.equals(other.toJson(), toJson());`,
2436
2398
  ``,
2437
2399
  ` @override`,
2438
- ` int get hashCode => _voJsonHash(toJson());`,
2400
+ ` int get hashCode => NmsInternalValueIdentity.hash(toJson());`,
2439
2401
  `}`,
2440
2402
  ].join("\n");
2441
2403
  }
@@ -2498,10 +2460,10 @@ function emitValueObjectClass(d: ValueObjectDescriptor): string {
2498
2460
  ``,
2499
2461
  ` @override`,
2500
2462
  ` bool operator ==(Object other) =>`,
2501
- ` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
2463
+ ` other is ${d.name} && NmsInternalValueIdentity.equals(other.toJson(), toJson());`,
2502
2464
  ``,
2503
2465
  ` @override`,
2504
- ` int get hashCode => _voJsonHash(toJson());`,
2466
+ ` int get hashCode => NmsInternalValueIdentity.hash(toJson());`,
2505
2467
  `}`,
2506
2468
  ].join("\n");
2507
2469
  }
@@ -5242,7 +5204,7 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
5242
5204
  ...(brandBlock !== "" ? [brandBlock] : []),
5243
5205
  ...(localValueObjects.length > 0
5244
5206
  ? [
5245
- emitVoHelpers(needsVoCopyWithSentinel),
5207
+ ...(needsVoCopyWithSentinel ? [emitVoHelpers()] : []),
5246
5208
  ...localValueObjects.flatMap((d) => [
5247
5209
  ...voEnumFields(d)
5248
5210
  .filter((e) => e.declaredName === undefined)
@@ -45,9 +45,13 @@ export const TS_RESULT_USDA_PRELUDE = [
45
45
  ` const c = source[i]!; if (c !== "\\\\") { out += c; continue; }`,
46
46
  ` const e = source[++i]; if (e === undefined) throw new Error("truncated USDA escape");`,
47
47
  ` if (e === "n") out += "\\n"; else if (e === "r") out += "\\r"; else if (e === "t") out += "\\t";`,
48
- ` else if (e === "b") out += "\\b"; else if (e === "f") out += "\\f"; else if (e === '"' || e === "\\\\") out += e;`,
49
- ` else if (e === "u") { const hex = source.slice(i + 1, i + 5); if (!/^[0-9a-fA-F]{4}$/.test(hex)) throw new Error("invalid USDA unicode escape"); out += String.fromCharCode(Number.parseInt(hex, 16)); i += 4; }`,
50
- ` else throw new Error("unsupported USDA escape: " + e);`,
48
+ // OpenUSD's escapes (TfEscapeString): named controls, \x with up to two hex digits, \NNN octal,
49
+ // any other escaped character is itself; plus the LEGACY \uXXXX that pre-CAP-862 kernels wrote.
50
+ ` else if (e === "b") out += "\\b"; else if (e === "f") out += "\\f"; else if (e === "a") out += "\\x07"; else if (e === "v") out += "\\v";`,
51
+ ` else if (e === "x") { const hex = /^[0-9a-fA-F]{0,2}/.exec(source.slice(i + 1, source.length - 1))![0]; out += String.fromCharCode(hex ? Number.parseInt(hex, 16) : 0); i += hex.length; }`,
52
+ ` else if (e >= "0" && e <= "7") { const oct = /^[0-7]{1,3}/.exec(source.slice(i, source.length - 1))![0]; out += String.fromCharCode(Number.parseInt(oct, 8)); i += oct.length - 1; }`,
53
+ ` else if (e === "u" && /^[0-9a-fA-F]{4}$/.test(source.slice(i + 1, i + 5)) && !/^d[89a-f]/i.test(source.slice(i + 1, i + 5))) { out += String.fromCharCode(Number.parseInt(source.slice(i + 1, i + 5), 16)); i += 4; }`,
54
+ ` else out += e;`,
51
55
  ` } return out;`,
52
56
  `};`,
53
57
  `const _nmsUsdaAtom = (raw: string): string | number | boolean | readonly string[] => {`,
@@ -71,7 +75,7 @@ export const TS_RESULT_USDA_PRELUDE = [
71
75
  ` if (declaration) { pending = { typeName: declaration[1]!, name: _nmsUsdaUnquote(declaration[2]!), properties: [], children: [] }; continue; }`,
72
76
  ` if (line === "{") { if (!pending) throw new Error("USDA scope has no declaration"); const parent = stack.at(-1); if (parent) parent.children.push(pending); else roots.push(pending); stack.push(pending); pending = undefined; continue; }`,
73
77
  ` if (line === "}") { if (!stack.pop()) throw new Error("unbalanced USDA scope"); continue; }`,
74
- ` const property = /^custom(?:\\s+uniform)?\\s+(?:rel|[A-Za-z0-9_\\[\\]]+)\\s+([A-Za-z0-9_:]+)\\s*=\\s*(.+)$/.exec(line);`,
78
+ ` const property = /^custom(?:\\s+uniform)?\\s+(?:rel|[A-Za-z0-9_\\[\\]]+)\\s+([A-Za-z0-9_:]+)\\s*=\\s*(.+)$/s.exec(line);`,
75
79
  ` if (property) { const owner = stack.at(-1); if (!owner) throw new Error("USDA property outside prim"); owner.properties.push({ name: property[1]!, value: _nmsUsdaAtom(property[2]!) }); continue; }`,
76
80
  ` throw new Error("unsupported canonical USDA line: " + line);`,
77
81
  ` }`,
@@ -1,5 +1,6 @@
1
1
  import { collectPackageSources, findBirthKeyWrites, formatBirthKeyWriteWarnings } from "./birth_key_rule.js";
2
2
  import type { Directive } from "./directive.js";
3
+ import { usdaString } from "./usda_text.js";
3
4
  // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
4
5
  // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
5
6
  // wasm32-wasip1 artifact {kernel · projection · embedded
@@ -817,7 +818,7 @@ function emitLawClosure(
817
818
  return [
818
819
  ` def NomosLawAsset "${primName}"`,
819
820
  " {",
820
- ` custom string nomos:logicalPath = ${JSON.stringify(
821
+ ` custom string nomos:logicalPath = ${usdaString(
821
822
  asset.logicalPath
822
823
  )}`,
823
824
  ` custom string nomos:sha256 = "${asset.sha256}"`,
@@ -2555,6 +2556,11 @@ const dartTypesImportFor = (generatedSource: string): string => {
2555
2556
  `import 'package:nomos_types/src/foreign_ref.dart' show NmsInternalForeignRefs;`,
2556
2557
  ]
2557
2558
  : []),
2559
+ ...(generatedSource.includes("NmsInternalValueIdentity.")
2560
+ ? [
2561
+ `import 'package:nomos_types/src/vo_canonical.dart' show NmsInternalValueIdentity;`,
2562
+ ]
2563
+ : []),
2558
2564
  ];
2559
2565
  const invitationHelpers = [
2560
2566
  ...(generatedSource.includes("NmsInternalInvitations.")
@@ -2578,12 +2584,12 @@ const dartTypesImportFor = (generatedSource: string): string => {
2578
2584
  * just the floor generated code needs — a lagging pin forces tenants into dependency_overrides. */
2579
2585
  // 0.3.0: SurfacedValue<T>/SurfacedObservation/PendingConflictRow — generated Surfaced-field typing +
2580
2586
  // the pendingConflicts read rows.
2581
- const NOMOS_TYPES_DEP = "^0.14.0";
2587
+ const NOMOS_TYPES_DEP = "^0.15.0";
2582
2588
  // 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
2583
2589
  // DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
2584
2590
  // REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
2585
- const NOMOS_CLIENT_DEP = "^0.69.0";
2586
- const NOMOS_FLUTTER_DEP = "^0.68.0";
2591
+ const NOMOS_CLIENT_DEP = "^0.70.1";
2592
+ const NOMOS_FLUTTER_DEP = "^0.68.2";
2587
2593
  // DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in the Dart and Flutter
2588
2594
  // package pubspecs — a release without this bump fails the suite, so tenants never
2589
2595
  // land back in dependency_overrides (the exact co2 report this closes, third occurrence).
@@ -3406,6 +3412,7 @@ function emitDartPackages(
3406
3412
  m[1] !== "package:nomos_types/src/versioned_nomos_ref.dart" &&
3407
3413
  m[1] !== "package:nomos_types/src/foreign_ref.dart" &&
3408
3414
  m[1] !== "package:nomos_types/src/invitation.dart" &&
3415
+ m[1] !== "package:nomos_types/src/vo_canonical.dart" &&
3409
3416
  m[1] !== `${FRAMEWORK_FILE}.dart` &&
3410
3417
  !siblingTypeFiles.has(m[1]!)
3411
3418
  ) {
@@ -93,7 +93,8 @@ import {
93
93
  assertApplicationLanguageCoversAuthoredDirectives,
94
94
  executableDirectivesForLanguage,
95
95
  publicOfferRoute,
96
- remapWorkspaceInvariantsForLanguage,
96
+ carryWorkspaceInvariantsToPerformingIntents,
97
+ isBusinessIntent,
97
98
  } from "./intent.js";
98
99
 
99
100
  /** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
@@ -511,9 +512,16 @@ function buildDomainSlice(mod: DomainModuleExports, domain: string): DomainSlice
511
512
  declaredWorkspaceInvariants.push(w);
512
513
  }
513
514
  }
514
- for (const w of applicationLanguage === undefined
515
- ? declaredWorkspaceInvariants
516
- : remapWorkspaceInvariantsForLanguage(declaredWorkspaceInvariants, applicationLanguage)) {
515
+ // CAP-863: the SAME carry rule package composition applies, with the same keep-the-original predicate —
516
+ // an effect's invariant is registered for every routable intent that performs it, every original is kept
517
+ // without a language, and a language keeps an original only while its target is still dispatchable. The
518
+ // registered set feeds the routing table, so it must equal the manifest's set: an extra body would emit a
519
+ // dead invariant route into language law, a missing one would fail closed at dispatch.
520
+ for (const w of carryWorkspaceInvariantsToPerformingIntents(
521
+ declaredWorkspaceInvariants,
522
+ [...dirs.values()].filter(isBusinessIntent),
523
+ (directiveId) => applicationLanguage === undefined || dirs.has(directiveId),
524
+ )) {
517
525
  wsInvariants.set(w.id, w);
518
526
  }
519
527
  // ── DESUGARED write-path preconditions (`.requiresExisting`/`.creates(Agg,idFn)`/`.where`) ──
@@ -12,6 +12,7 @@
12
12
  // NO JSON, INCLUDING FOR OPAQUE DESCRIPTORS. A field's canonical value `shape` and merge `driver`
13
13
  // are compiler-derived structured values. They are written here through the generic typed-USDA value
14
14
  // codec below rather than stringified, so this file has no JSON in it at any depth.
15
+ import { usdaString, usdaUnquote } from "./usda_text.js";
15
16
  import {
16
17
  emptyLawIdentityRegistry,
17
18
  lawEntityKeyParts,
@@ -28,58 +29,8 @@ export const NOMOS_IDENTITY_LAYER_FORMAT = "nomos.identity.v1";
28
29
  /** The committed identity layer's filename, beside the package config. */
29
30
  export const NOMOS_IDENTITY_FILENAME = "nomos.identity.usda";
30
31
 
31
- function quote(value: string): string {
32
- return `"${value.replace(/[\\"\u0000-\u001f]/gu, (character) => {
33
- switch (character) {
34
- case "\\": return "\\\\";
35
- case '"': return '\\"';
36
- case "\n": return "\\n";
37
- case "\r": return "\\r";
38
- case "\t": return "\\t";
39
- case "\b": return "\\b";
40
- case "\f": return "\\f";
41
- default: return `\\u${character.charCodeAt(0).toString(16).padStart(4, "0")}`;
42
- }
43
- })}"`;
44
- }
45
-
46
- function parseQuoted(raw: string): string {
47
- const value = raw.trim();
48
- if (!value.startsWith('"') || !value.endsWith('"')) {
49
- throw new Error(`expected a quoted USDA string, got ${value}`);
50
- }
51
- let out = "";
52
- for (let index = 1; index < value.length - 1; index += 1) {
53
- const character = value[index]!;
54
- if (character !== "\\") {
55
- out += character;
56
- continue;
57
- }
58
- index += 1;
59
- const escaped = value[index];
60
- if (escaped === undefined || index >= value.length - 1) throw new Error("unterminated USDA escape");
61
- if (escaped === "u") {
62
- const digits = value.slice(index + 1, index + 5);
63
- if (!/^[0-9a-fA-F]{4}$/u.test(digits)) throw new Error(`invalid USDA unicode escape \\u${digits}`);
64
- out += String.fromCharCode(Number.parseInt(digits, 16));
65
- index += 4;
66
- continue;
67
- }
68
- const decoded: Record<string, string> = {
69
- "\\": "\\",
70
- '"': '"',
71
- n: "\n",
72
- r: "\r",
73
- t: "\t",
74
- b: "\b",
75
- f: "\f",
76
- };
77
- const replacement = decoded[escaped];
78
- if (replacement === undefined) throw new Error(`unsupported USDA escape \\${escaped}`);
79
- out += replacement;
80
- }
81
- return out;
82
- }
32
+ const quote = usdaString;
33
+ const parseQuoted = usdaUnquote;
83
34
 
84
35
  function parseStringList(raw: string): string[] {
85
36
  const value = raw.trim();
package/src/index.ts CHANGED
@@ -147,7 +147,17 @@ export {
147
147
  type GeoPolygonValue,
148
148
  type GeoMultiPointValue,
149
149
  type ValueObjectUsage,
150
+ valueIdentity,
150
151
  } from "./value_object.js";
152
+ // VALUE IDENTITY (`value_identity.ts`, CAP-853): every value object's `canonical` / `equals` /
153
+ // `digest` implement the versioned `nomos.vo-canonical.v1` encoding (RFC 8785 over the
154
+ // descriptor-judged value); `valueIdentity(schema)` derives them for lists/maps of VOs.
155
+ export {
156
+ VO_CANONICAL_ENCODING,
157
+ VoCanonicalError,
158
+ type ValueIdentity,
159
+ type VoCanonicalEncoding,
160
+ } from "./value_identity.js";
151
161
  export {
152
162
  enumType,
153
163
  enumTypeNameOf,
package/src/intent.ts CHANGED
@@ -1449,17 +1449,42 @@ export function assertApplicationLanguageCoversAuthoredDirectives(
1449
1449
  export function remapWorkspaceInvariantsForLanguage(
1450
1450
  invariants: readonly WorkspaceInvariantDecl[],
1451
1451
  language: ApplicationLanguage
1452
+ ): WorkspaceInvariantDecl[] {
1453
+ return carryWorkspaceInvariantsToPerformingIntents(
1454
+ invariants,
1455
+ language.intents,
1456
+ () => false
1457
+ );
1458
+ }
1459
+
1460
+ /**
1461
+ * THE ONE CARRY RULE for effect invariants (CAP-863). The kernel selects a workspace invariant by
1462
+ * the DISPATCHED directive id (`invariant.on == directive_id`), and a business intent dispatches
1463
+ * under its own id while planning its effects inline. An invariant declared `.on(effect)` therefore
1464
+ * judges an intent only if it is carried onto that intent. Carry it onto EVERY routable business
1465
+ * intent that performs the effect, whether or not the domain declares `language(...)`.
1466
+ *
1467
+ * The original declaration stays on the effect when nothing performs it, or when the caller says the
1468
+ * effect id may still be dispatched (`dispatchable(effect.id)`): a public directive dispatched directly,
1469
+ * or (without a language) a bare id another installed law may dispatch, must still be judged. A
1470
+ * language-closed effect is private, so its original is dropped exactly as before. Package composition
1471
+ * and the sealed engine both call this function with the same predicate, so the manifest and the
1472
+ * executable invariant registry (and its routing table) name exactly the same invariants.
1473
+ */
1474
+ export function carryWorkspaceInvariantsToPerformingIntents(
1475
+ invariants: readonly WorkspaceInvariantDecl[],
1476
+ intents: readonly BusinessIntent<any>[],
1477
+ dispatchable: (directiveId: string) => boolean
1452
1478
  ): WorkspaceInvariantDecl[] {
1453
1479
  const out: WorkspaceInvariantDecl[] = [];
1454
1480
  for (const invariant of invariants) {
1455
- const uses = language.intents.flatMap((candidate) =>
1481
+ const uses = intents.flatMap((candidate) =>
1456
1482
  candidate.steps
1457
1483
  .filter((step) => step.directive.id === invariant.on)
1458
1484
  .map((step) => ({ intent: candidate, step }))
1459
1485
  );
1460
- if (uses.length === 0) {
1486
+ if (uses.length === 0 || dispatchable(invariant.on)) {
1461
1487
  out.push(invariant);
1462
- continue;
1463
1488
  }
1464
1489
  for (const use of uses) {
1465
1490
  const mapFacts = (
package/src/law_check.ts CHANGED
@@ -1103,6 +1103,39 @@ function invariantFindings(mod: DomainModule, aggById: ReadonlyMap<string, Aggre
1103
1103
  return out;
1104
1104
  }
1105
1105
 
1106
+ // ── VALUE IDENTITY (CAP-853) — `JSON.stringify` COMPARISON WARN ──────────────────────────
1107
+ // `JSON.stringify(a) === JSON.stringify(b)` compares TEXT, and json text depends on member insertion
1108
+ // order: a value object read back from storage arrives in storage order, so the comparison calls a
1109
+ // value unequal to itself (CO2's field-publication invariant refused every round-tripped snapshot).
1110
+ // Law compares typed values with `<Vo>.equals(a, b)` (`nomos.vo-canonical.v1`). This is a source-text
1111
+ // heuristic over the law bodies Nomos runs (plans, aggregate invariants, workspace invariants), so it
1112
+ // WARNS — it cannot prove the operands are structured values.
1113
+ const JSON_STRINGIFY_COMPARISON =
1114
+ /JSON\.stringify\s*\((?:[^()]|\((?:[^()]|\([^()]*\))*\))*\)\s*[!=]==?|[!=]==?\s*JSON\.stringify\s*\(/;
1115
+
1116
+ function jsonStringifyComparisonFindings(mod: DomainModule): Finding[] {
1117
+ const domain = mod.domain ?? mod.name;
1118
+ const out: Finding[] = [];
1119
+ const judge = (body: unknown, subject: string, directiveId?: string): void => {
1120
+ if (typeof body !== "function") return;
1121
+ if (!JSON_STRINGIFY_COMPARISON.test(Function.prototype.toString.call(body))) return;
1122
+ out.push({
1123
+ domain,
1124
+ ...(directiveId === undefined ? {} : { directiveId }),
1125
+ level: "warn",
1126
+ what: `${subject} compares JSON.stringify(...) text — member order is not part of a value, so a value read back from storage can compare unequal to itself`,
1127
+ fix: "compare typed values with <ValueObject>.equals(a, b) (or valueIdentity(schema).equals for lists/maps of them); for plain json leaves compare the fields you mean",
1128
+ });
1129
+ };
1130
+ for (const dir of mod.directives as Directive<unknown>[]) judge(dir.plan, `directive '${dir.id}' plan`, dir.id);
1131
+ for (const agg of mod.aggregates) judge(agg.invariant, `aggregate '${agg.id}' invariant`);
1132
+ for (const wi of mod.workspaceInvariants ?? []) {
1133
+ judge(wi.assert, `workspace invariant '${wi.id}' assert`, wi.on);
1134
+ judge(wi.reads, `workspace invariant '${wi.id}' reads`, wi.on);
1135
+ }
1136
+ return out;
1137
+ }
1138
+
1106
1139
  // ── TWIN 2 (slice 2) — TOP-LEVEL PORT-READ pool-eligibility WARN ───────────────────
1107
1140
  // A tenant MODULE whose TOP LEVEL reads the captured ports (`Date.now()`/`new Date()`/`Math.random()`
1108
1141
  // at module scope, not inside a plan) makes the built engine lump PRISTINE-POOL-INELIGIBLE
@@ -1359,6 +1392,7 @@ export function checkDomainModules(
1359
1392
  findings.push(...nullBanFindings(mod, aggById));
1360
1393
  findings.push(...nullClearAuthorityFindings(mod, aggById));
1361
1394
  findings.push(...attestedReadFindings(mod));
1395
+ findings.push(...jsonStringifyComparisonFindings(mod));
1362
1396
  }
1363
1397
 
1364
1398
  // The BIRTH-RECIPE twin (unify): fold the scattered `.births()` recipe diagnostics into the
@@ -1,3 +1,4 @@
1
+ import { usdaString } from "./usda_text.js";
1
2
  import type {
2
3
  LawAggregateNode,
3
4
  LawAnonymousValueFieldNode,
@@ -1946,9 +1947,7 @@ function requiredPath(id: string, paths: ReadonlyMap<string, string>): string {
1946
1947
  return path;
1947
1948
  }
1948
1949
 
1949
- function quote(value: string): string {
1950
- return JSON.stringify(value);
1951
- }
1950
+ const quote = usdaString;
1952
1951
 
1953
1952
  function fail(message: string): never {
1954
1953
  throw new Error(`NMS-USDA-001 ${message}`);
@@ -10,6 +10,7 @@
10
10
  // subset and throws on anything else. Law layers carry relationships, dictionaries and typed
11
11
  // literals that layer never contains, so the baseline export needs a reader that keeps going past
12
12
  // opinions it does not understand instead of refusing a whole released layer over one of them.
13
+ import { usdaUnquote } from "./usda_text.js";
13
14
 
14
15
  export interface UsdaPrim {
15
16
  /** The prim's name, unquoted. */
@@ -29,35 +30,9 @@ const PRIM_OPEN = /^\s*(?:def|over|class)\b(?:\s+([A-Za-z_][A-Za-z0-9_]*))?\s+"(
29
30
  const REL = /^\s*(?:custom\s+)?(?:uniform\s+)?rel\s+nomos:([A-Za-z0-9_]+)\s*=\s*(.+?)\s*$/;
30
31
  const ATTR = /^\s*(?:custom\s+)?(?:uniform\s+)?[A-Za-z0-9_]+(?:\[\])?\s+nomos:([A-Za-z0-9_]+)\s*=\s*(.+?)\s*$/;
31
32
 
32
- /** Decode a USDA quoted string literal (the escapes the compiler writes). */
33
+ /** Decode a USDA quoted string literal with OpenUSD's escape rules (and legacy `\uXXXX`). */
33
34
  export function unquoteUsda(raw: string): string {
34
- const value = raw.trim();
35
- if (!value.startsWith('"') || !value.endsWith('"') || value.length < 2) {
36
- throw new Error(`expected a quoted USDA string, got ${value}`);
37
- }
38
- let out = "";
39
- for (let index = 1; index < value.length - 1; index += 1) {
40
- const character = value[index]!;
41
- if (character !== "\\") {
42
- out += character;
43
- continue;
44
- }
45
- index += 1;
46
- const escaped = value[index];
47
- if (escaped === undefined) throw new Error("unterminated USDA escape");
48
- if (escaped === "u") {
49
- const digits = value.slice(index + 1, index + 5);
50
- if (!/^[0-9a-fA-F]{4}$/u.test(digits)) throw new Error(`invalid USDA unicode escape \\u${digits}`);
51
- out += String.fromCharCode(Number.parseInt(digits, 16));
52
- index += 4;
53
- continue;
54
- }
55
- const decoded: Record<string, string> = { "\\": "\\", '"': '"', n: "\n", r: "\r", t: "\t", b: "\b", f: "\f" };
56
- const replacement = decoded[escaped];
57
- if (replacement === undefined) throw new Error(`unsupported USDA escape \\${escaped}`);
58
- out += replacement;
59
- }
60
- return out;
35
+ return usdaUnquote(raw);
61
36
  }
62
37
 
63
38
  /** Decode a one-line USDA string array literal `["a", "b"]`. */
package/src/law_units.ts CHANGED
@@ -10,6 +10,7 @@
10
10
  // (kernel/src/law_units.rs) makes from the same package, so both hash identically. The format is
11
11
  // specified in architecture/law_units_format.md.
12
12
 
13
+ import { usdaString, usdaUnquote } from "./usda_text.js";
13
14
  import { createHash } from "node:crypto";
14
15
 
15
16
  /** One authenticated file of a law closure, at its package logical path. */
@@ -136,7 +137,7 @@ export function scanDomainDeclarations(usda: string): DomainDeclarations {
136
137
  if (frames.includes(null)) continue;
137
138
  const at = frames as string[];
138
139
  if (at.length !== 5 || at[0] !== "Nomos" || at[1] !== "Domains") continue;
139
- const value = JSON.parse(property[2]!) as string;
140
+ const value = usdaUnquote(property[2]!);
140
141
  if (at[3] === "Aggregates") {
141
142
  (property[1] === "wireType" ? aggregateWireTypes : aggregateLabels).add(value);
142
143
  } else if (at[3] === "Invariants" && property[1] === "label") {
@@ -213,7 +214,7 @@ export function packageRoutesLayerUsda(routes: readonly LawUnitRoute[]): string
213
214
  routeOpinions.push(
214
215
  ` def Scope "r${String(index).padStart(6, "0")}"`,
215
216
  " {",
216
- ` custom string nomos:selector = ${JSON.stringify(
217
+ ` custom string nomos:selector = ${usdaString(
217
218
  route.selector
218
219
  )}`,
219
220
  ` custom string nomos:plan = "${route.planSha256}"`,
@@ -268,11 +269,11 @@ export function unitManifestUsda(unit: Omit<LawUnit, "manifest">): string {
268
269
  "",
269
270
  'def NomosLawUnit "__NomosLawUnit_7e9c"',
270
271
  "{",
271
- ` custom string nomos:unit = ${JSON.stringify(unit.key)}`,
272
+ ` custom string nomos:unit = ${usdaString(unit.key)}`,
272
273
  ` custom token nomos:unitKind = "${unit.kind}"`,
273
274
  ...(unit.domain === undefined
274
275
  ? []
275
- : [` custom string nomos:domain = ${JSON.stringify(unit.domain)}`]),
276
+ : [` custom string nomos:domain = ${usdaString(unit.domain)}`]),
276
277
  ` custom string nomos:unitHash = "${unit.hash}"`,
277
278
  "",
278
279
  ' def Scope "Assets"',
@@ -280,7 +281,7 @@ export function unitManifestUsda(unit: Omit<LawUnit, "manifest">): string {
280
281
  ...unit.assets.flatMap((asset) => [
281
282
  ` def NomosLawAsset "a_${sha256Hex(asset.logicalPath)}"`,
282
283
  " {",
283
- ` custom string nomos:logicalPath = ${JSON.stringify(asset.logicalPath)}`,
284
+ ` custom string nomos:logicalPath = ${usdaString(asset.logicalPath)}`,
284
285
  ` custom string nomos:sha256 = "${asset.sha256}"`,
285
286
  " }",
286
287
  ]),
@@ -291,7 +292,7 @@ export function unitManifestUsda(unit: Omit<LawUnit, "manifest">): string {
291
292
  ...unit.routes.flatMap((route) => [
292
293
  ` def NomosLawRoute "s_${sha256Hex(route.selector)}"`,
293
294
  " {",
294
- ` custom string nomos:selector = ${JSON.stringify(route.selector)}`,
295
+ ` custom string nomos:selector = ${usdaString(route.selector)}`,
295
296
  ` custom string nomos:plan = "${route.planSha256}"`,
296
297
  " }",
297
298
  ]),
@@ -15,6 +15,7 @@
15
15
  // Grammar: semver 2.0 MAJOR.MINOR.PATCH with an optional pre-release, no build metadata (build metadata does not
16
16
  // order versions, so it could only make two different laws claim the same place). Dependency-free so the compiler,
17
17
  // CLI, console and Peer Office share one comparison; the kernel's twin is `law_version` (kernel/src/law_version.rs).
18
+ import { usdaUnquote } from "./usda_text.js";
18
19
 
19
20
  /** A validated claim, as authored: typed, never a bare string. Only `lawVersionClaim` and the readers here mint one. */
20
21
  export type LawVersionClaim = string & { readonly __nomosLawVersionClaim: unique symbol };
@@ -103,5 +104,5 @@ export function compareLawVersions(left: LawVersionClaim, right: LawVersionClaim
103
104
  export function claimedVersionOfPackageContract(packageUsda: string): LawVersionClaim | undefined {
104
105
  const match = /^\s*custom uniform string nomos:version = ("(?:[^"\\]|\\.)*")\s*$/mu.exec(packageUsda);
105
106
  if (match === null) return undefined;
106
- return lawVersionClaim(JSON.parse(match[1]!) as string);
107
+ return lawVersionClaim(usdaUnquote(match[1]!));
107
108
  }
@@ -2,6 +2,7 @@
2
2
 
3
3
  import type { AggregateHandle, FieldValue, StoredFields } from "./aggregate.js";
4
4
  import type { Field } from "./fields.js";
5
+ import { canonicalJsonValue } from "./value_identity.js";
5
6
 
6
7
  export type PointGuardScalar = string | number | boolean;
7
8
 
@@ -185,7 +186,10 @@ export function evaluatePointGuard(
185
186
  ? predicate.right.value
186
187
  : valueAt(predicate.right.source === "row" ? row : payload, predicate.right.path);
187
188
  if (left === undefined || right === undefined) return predicate.operator === "ne";
188
- const equal = JSON.stringify(left) === JSON.stringify(right);
189
+ // Value equality, not text equality: a stored row's members come back in storage order, so
190
+ // `JSON.stringify` would call a value unequal to itself (CAP-853). The kernel twin compares
191
+ // structurally too (`admission-peer` nominal_equals_json / serde_json Value equality).
192
+ const equal = canonicalJsonValue(left) === canonicalJsonValue(right);
189
193
  return predicate.operator === "eq" ? equal : !equal;
190
194
  }
191
195
  }
@@ -1,4 +1,6 @@
1
1
  /** Read-only evidence, not an evolution/admission judge. No compile, mint or network calls. */
2
+ import { usdaUnquote } from './usda_text.js';
3
+ import { unquoteUsdaList } from './law_layer_prims.js';
2
4
  import { createHash } from 'node:crypto';
3
5
  import { lstatSync, readFileSync, readdirSync, realpathSync } from 'node:fs';
4
6
  import path from 'node:path';
@@ -12,7 +14,7 @@ const literal = '("(?:[^"\\\\]|\\\\.)*")';
12
14
  function attribute(source: string, name: string): string {
13
15
  const values = [...source.matchAll(new RegExp(`nomos:${name} = ${literal}`, 'g'))];
14
16
  if (values.length !== 1) throw new Error(`Expected one nomos:${name}`);
15
- return JSON.parse(values[0]![1]!);
17
+ return usdaUnquote(values[0]![1]!);
16
18
  }
17
19
  function files(directory: string, root = directory): string[] {
18
20
  return readdirSync(directory).sort().flatMap(name => {
@@ -92,9 +94,9 @@ export function inspectReleaseBaseline(input: { identity: string; units: string;
92
94
  const label = /nomos:label = ("(?:[^"\\]|\\.)*")/.exec(attrs);
93
95
  const wire = /nomos:wireKey = ("(?:[^"\\]|\\.)*")/.exec(attrs);
94
96
  const aliases = /nomos:wireAliases = (\[[^\n]*\])/.exec(attrs);
95
- const item = { ...(label ? { label: JSON.parse(label[1]!) as string } : {}),
96
- ...(wire ? { wire: JSON.parse(wire[1]!) as string } : {}),
97
- ...(aliases ? { aliases: JSON.parse(aliases[1]!) as string[] } : {}) };
97
+ const item = { ...(label ? { label: usdaUnquote(label[1]!) } : {}),
98
+ ...(wire ? { wire: usdaUnquote(wire[1]!) } : {}),
99
+ ...(aliases ? { aliases: unquoteUsdaList(aliases[1]!) } : {}) };
98
100
  for (const key of [match[1]!, ...(wire ? [`u_${match[2]!}`] : [])]) {
99
101
  declarations.set(key, [...(declarations.get(key) ?? []), item]);
100
102
  }