@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
package/src/codegen_call_usda.ts
CHANGED
|
@@ -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 ||
|
|
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 ||
|
|
258
|
-
` out.write('\\\\
|
|
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
|
` }`,
|
package/src/codegen_dart.ts
CHANGED
|
@@ -2133,54 +2133,16 @@ function voToJsonExpr(f: ValueObjectFieldDescriptor, expr: string, depth = 0): s
|
|
|
2133
2133
|
}
|
|
2134
2134
|
}
|
|
2135
2135
|
|
|
2136
|
-
/**
|
|
2137
|
-
* per file when
|
|
2138
|
-
|
|
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
|
-
|
|
2141
|
-
|
|
2142
|
-
|
|
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} &&
|
|
2226
|
+
` bool operator ==(Object other) => other is ${cls} && NmsInternalValueIdentity.equals(other.toJson(), toJson());`,
|
|
2265
2227
|
` @override`,
|
|
2266
|
-
` int get hashCode =>
|
|
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} &&
|
|
2278
|
+
` bool operator ==(Object other) => other is ${cls} && NmsInternalValueIdentity.equals(other.toJson(), toJson());`,
|
|
2317
2279
|
` @override`,
|
|
2318
|
-
` int get hashCode =>
|
|
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 &&
|
|
2297
|
+
` bool operator ==(Object other) => other is ${d.name}Unknown && NmsInternalValueIdentity.equals(other.raw, raw);`,
|
|
2336
2298
|
` @override`,
|
|
2337
|
-
` int get hashCode =>
|
|
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} &&
|
|
2397
|
+
` other is ${d.name} && NmsInternalValueIdentity.equals(other.toJson(), toJson());`,
|
|
2436
2398
|
``,
|
|
2437
2399
|
` @override`,
|
|
2438
|
-
` int get hashCode =>
|
|
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} &&
|
|
2463
|
+
` other is ${d.name} && NmsInternalValueIdentity.equals(other.toJson(), toJson());`,
|
|
2502
2464
|
``,
|
|
2503
2465
|
` @override`,
|
|
2504
|
-
` int get hashCode =>
|
|
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(
|
|
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
|
-
|
|
49
|
-
|
|
50
|
-
` else
|
|
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*(.+)
|
|
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 = ${
|
|
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.
|
|
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.
|
|
2586
|
-
const NOMOS_FLUTTER_DEP = "^0.68.
|
|
2591
|
+
const NOMOS_CLIENT_DEP = "^0.70.2";
|
|
2592
|
+
const NOMOS_FLUTTER_DEP = "^0.68.4";
|
|
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
|
) {
|
package/src/engine_entry.ts
CHANGED
|
@@ -93,7 +93,8 @@ import {
|
|
|
93
93
|
assertApplicationLanguageCoversAuthoredDirectives,
|
|
94
94
|
executableDirectivesForLanguage,
|
|
95
95
|
publicOfferRoute,
|
|
96
|
-
|
|
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
|
-
|
|
515
|
-
|
|
516
|
-
|
|
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`) ──
|
package/src/identity_usda.ts
CHANGED
|
@@ -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
|
-
|
|
32
|
-
|
|
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 =
|
|
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
|
package/src/law_graph_usda.ts
CHANGED
|
@@ -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
|
-
|
|
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}`);
|
package/src/law_layer_prims.ts
CHANGED
|
@@ -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
|
|
33
|
+
/** Decode a USDA quoted string literal with OpenUSD's escape rules (and legacy `\uXXXX`). */
|
|
33
34
|
export function unquoteUsda(raw: string): string {
|
|
34
|
-
|
|
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 =
|
|
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 = ${
|
|
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 = ${
|
|
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 = ${
|
|
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 = ${
|
|
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 = ${
|
|
295
|
+
` custom string nomos:selector = ${usdaString(route.selector)}`,
|
|
295
296
|
` custom string nomos:plan = "${route.planSha256}"`,
|
|
296
297
|
" }",
|
|
297
298
|
]),
|
package/src/law_version.ts
CHANGED
|
@@ -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(
|
|
107
|
+
return lawVersionClaim(usdaUnquote(match[1]!));
|
|
107
108
|
}
|
package/src/point_guard.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
|
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:
|
|
96
|
-
...(wire ? { wire:
|
|
97
|
-
...(aliases ? { aliases:
|
|
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
|
}
|