@githolon/dsl 0.114.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.
- package/assets/operation-contract.wasm +0 -0
- package/dart/CHANGELOG.md +55 -0
- package/dart/lib/src/application_framework_calls.dart +7 -1
- package/dart/lib/src/console_enrolment.dart +7 -0
- package/dart/lib/src/console_http_io.dart +22 -15
- package/dart/lib/src/dev_cloud.dart +62 -0
- package/dart/lib/src/nms_generated_runtime.dart +1 -0
- package/dart/lib/src/operation_receipts.dart +74 -7
- package/dart/lib/src/reception/dart_reception.dart +26 -6
- package/dart/lib/src/result_usda.dart +64 -27
- package/dart/lib/src/runner/node-entry.mjs +4733 -1534
- package/dart/lib/src/runtime_v2_transport.dart +3 -0
- 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/console_enrolment_test.dart +47 -0
- package/dart/test/kernel_refused_follow_test.dart +36 -0
- package/dart/test/model_adoption_test.dart +49 -0
- package/dart/test/result_usda_test.dart +67 -0
- package/dart/test/runtime_projections_test.dart +41 -0
- package/dart_types/CHANGELOG.md +30 -0
- package/dart_types/lib/src/approved_model.dart +78 -1
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +528 -2
- package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +24 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +1897 -112
- package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +98 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +11 -1
- package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +16 -0
- package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +43 -2
- 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 +4 -2
- package/src/aggregate.ts +95 -0
- package/src/anonymous_value_interning.ts +205 -0
- package/src/baseline.ts +27 -0
- package/src/baseline_export.ts +432 -0
- package/src/baseline_import.ts +312 -0
- package/src/baseline_usda.ts +185 -0
- package/src/birth_key_rule.ts +472 -0
- package/src/births.ts +207 -24
- package/src/build_package.ts +47 -7
- package/src/build_provenance.ts +10 -0
- package/src/business_birth.ts +50 -4
- package/src/check_package_main.ts +50 -6
- package/src/codegen_call_proto.ts +5 -18
- package/src/codegen_call_usda.ts +6 -3
- package/src/codegen_dart.ts +114 -60
- package/src/codegen_dart_client.ts +52 -6
- package/src/codegen_dart_facade.ts +57 -35
- package/src/codegen_dart_framework_calls.ts +20 -1
- package/src/codegen_proof.ts +5 -2
- package/src/codegen_result_proto.ts +8 -6
- package/src/codegen_result_usda.ts +12 -4
- package/src/codegen_ts.ts +109 -10
- package/src/compatibility_graph.ts +41 -4
- package/src/compatibility_usda.ts +11 -1
- package/src/compile_package_main.ts +222 -52
- package/src/composed_wire_identity.ts +24 -0
- package/src/directive.ts +6 -0
- package/src/domain_requirements.ts +75 -0
- package/src/engine_entry.ts +12 -4
- package/src/engine_javascript.ts +12 -6
- package/src/fields.ts +109 -0
- package/src/identity_usda.ts +80 -103
- package/src/index.ts +35 -1
- package/src/intent.ts +33 -3
- package/src/law_check.ts +34 -0
- package/src/law_graph.ts +142 -1
- package/src/law_graph_usda.ts +27 -144
- package/src/law_layer_prims.ts +174 -0
- package/src/law_unit_selection.ts +45 -0
- package/src/law_units.ts +7 -6
- package/src/law_version.ts +108 -0
- package/src/package_config.ts +44 -0
- package/src/package_dependencies.ts +8 -0
- 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 +35 -4
- package/src/wire.ts +2 -0
- package/src/wire_paths.ts +17 -0
- package/src/workspace_routing.ts +1 -1
|
@@ -0,0 +1,355 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
|
|
5
|
+
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* VALUE IDENTITY FOR TYPED VALUE OBJECTS — `nomos.vo-canonical.v1` (CAP-853).
|
|
10
|
+
*
|
|
11
|
+
* A value object's DESCRIPTOR is the canonical identity of its SHAPE. This module gives its
|
|
12
|
+
* VALUES one canonical identity too, so law can ask "is this the same value?" and "what is this
|
|
13
|
+
* value's content digest?" without `JSON.stringify` (whose bytes depend on key insertion order,
|
|
14
|
+
* and so change when a value round-trips through storage).
|
|
15
|
+
*
|
|
16
|
+
* THE ENCODING (`nomos.vo-canonical.v1`) is RFC 8785 (JSON Canonicalization Scheme) applied to the
|
|
17
|
+
* value AFTER the VO's descriptor has judged it:
|
|
18
|
+
*
|
|
19
|
+
* - object members are emitted in UTF-16 code-unit order of their names (RFC 8785 §3.2.3);
|
|
20
|
+
* - numbers use the ECMAScript Number-to-String spelling (RFC 8785 §3.2.2.3): `1`, `1.5`,
|
|
21
|
+
* `1e+21`, `1e-7`; integer and float fields share that one spelling, so a float stored as a
|
|
22
|
+
* double and read back as `2` is the same value as the authored `2`; `-0` is `0`;
|
|
23
|
+
* NaN and ±Infinity are refused;
|
|
24
|
+
* - strings use the RFC 8785 escapes (`\"`, `\\`, `\b \f \n \r \t`, other controls `\u00xx`,
|
|
25
|
+
* everything else verbatim); a string that is not well-formed UTF-16 (a lone surrogate) is
|
|
26
|
+
* refused, so its UTF-8 bytes are always defined;
|
|
27
|
+
* - ABSENT IS NOT NULL. An absent optional member (missing, or present as `undefined`) is
|
|
28
|
+
* omitted, exactly as storage omits it (`wire_encode.ts`); `null` is emitted only where the
|
|
29
|
+
* descriptor declares the member nullable and is otherwise refused. `{}` and `{"a":null}` are
|
|
30
|
+
* different values;
|
|
31
|
+
* - a `variant(...)` carries its case in the `type` member (and a named-VO arm nests its value
|
|
32
|
+
* under `value`), so the chosen case is always part of the encoding;
|
|
33
|
+
* - `idOf(...)`, `brandedId(...)` and units are their wire strings/numbers; a `sha256Digest()`
|
|
34
|
+
* member must be 64 lowercase hex characters;
|
|
35
|
+
* - list order is significant; map (`z.record`) entry order is not.
|
|
36
|
+
*
|
|
37
|
+
* Validation is part of the encoding: a value the descriptor does not admit (an undeclared member,
|
|
38
|
+
* a missing required member, an enum outside its values, a non-integer in an `.int()` field, a
|
|
39
|
+
* wrong variant case) has NO canonical form and throws. Equality is byte equality of the encoding.
|
|
40
|
+
* The DIGEST is SHA-256 over the UTF-8 bytes of the encoding, computed through the deterministic
|
|
41
|
+
* `nomos.sha256` port of the sealed engine (the same port that names keyed births), so every peer
|
|
42
|
+
* and every replay derives the same digest. Outside an engine there is no fallback: a test installs
|
|
43
|
+
* the port. A future change to any rule above is a NEW encoding id; a stored digest should keep
|
|
44
|
+
* {@link VO_CANONICAL_ENCODING} beside it.
|
|
45
|
+
*
|
|
46
|
+
* Conformance vectors: `dsl/conformance/vo_canonical.v1.json`, replayed by the TypeScript suite,
|
|
47
|
+
* the sealed engine (`testing/test/value_contract.test.mjs`) and the Dart runtime
|
|
48
|
+
* (`dsl/dart/test/vo_canonical_test.dart`). Keep this file node-builtin-free: it rides the runtime
|
|
49
|
+
* barrel into the engine bundle.
|
|
50
|
+
*/
|
|
51
|
+
|
|
52
|
+
import type {
|
|
53
|
+
GeometryKind,
|
|
54
|
+
Sha256Digest,
|
|
55
|
+
ValueObjectDescriptor,
|
|
56
|
+
ValueObjectFieldDescriptor,
|
|
57
|
+
} from "./fields.js";
|
|
58
|
+
|
|
59
|
+
/** The versioned id of the canonical value encoding. A change to the encoding is a new id. */
|
|
60
|
+
export const VO_CANONICAL_ENCODING = "nomos.vo-canonical.v1" as const;
|
|
61
|
+
export type VoCanonicalEncoding = typeof VO_CANONICAL_ENCODING;
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* The value-identity operations every value object carries (and {@link valueIdentity} derives for
|
|
65
|
+
* lists and maps of them). Usable inside `.plan()` and invariant `.assert(...)` bodies.
|
|
66
|
+
*/
|
|
67
|
+
export interface ValueIdentity<T> {
|
|
68
|
+
/** The encoding these operations implement (`nomos.vo-canonical.v1`). */
|
|
69
|
+
readonly encoding: VoCanonicalEncoding;
|
|
70
|
+
/** The canonical encoding of `value` (throws when the descriptor does not admit the value). */
|
|
71
|
+
canonical(value: T): string;
|
|
72
|
+
/**
|
|
73
|
+
* Value equality over the canonical encoding. `undefined` (an absent optional) equals only
|
|
74
|
+
* `undefined`; a value the descriptor does not admit throws rather than comparing unequal.
|
|
75
|
+
*/
|
|
76
|
+
equals(a: T | undefined, b: T | undefined): boolean;
|
|
77
|
+
/** SHA-256 (lowercase hex) of the canonical encoding's UTF-8 bytes, via the engine's port. */
|
|
78
|
+
digest(value: T): Sha256Digest;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** The value-object descriptors a value may reference, by name. */
|
|
82
|
+
export type ValueObjectTypes = ReadonlyMap<string, ValueObjectDescriptor>;
|
|
83
|
+
|
|
84
|
+
export class VoCanonicalError extends TypeError {
|
|
85
|
+
constructor(at: string, problem: string) {
|
|
86
|
+
super(`${VO_CANONICAL_ENCODING}: ${at} ${problem}`);
|
|
87
|
+
this.name = "VoCanonicalError";
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
// ── RFC 8785 primitives ───────────────────────────────────────────────────────────────
|
|
92
|
+
|
|
93
|
+
const PLAIN_STRING = /^[^"\\\u0000-\u001f\ud800-\udfff]*$/;
|
|
94
|
+
|
|
95
|
+
function canonicalString(value: string, at: string): string {
|
|
96
|
+
if (PLAIN_STRING.test(value)) return `"${value}"`;
|
|
97
|
+
let out = '"';
|
|
98
|
+
for (let i = 0; i < value.length; i++) {
|
|
99
|
+
const c = value.charCodeAt(i);
|
|
100
|
+
if (c >= 0xd800 && c <= 0xdbff) {
|
|
101
|
+
const next = i + 1 < value.length ? value.charCodeAt(i + 1) : -1;
|
|
102
|
+
if (next < 0xdc00 || next > 0xdfff) throw new VoCanonicalError(at, "is not well-formed UTF-16 (a lone high surrogate)");
|
|
103
|
+
out += value[i]! + value[i + 1]!;
|
|
104
|
+
i++;
|
|
105
|
+
continue;
|
|
106
|
+
}
|
|
107
|
+
if (c >= 0xdc00 && c <= 0xdfff) throw new VoCanonicalError(at, "is not well-formed UTF-16 (a lone low surrogate)");
|
|
108
|
+
switch (c) {
|
|
109
|
+
case 0x22: out += '\\"'; break;
|
|
110
|
+
case 0x5c: out += "\\\\"; break;
|
|
111
|
+
case 0x08: out += "\\b"; break;
|
|
112
|
+
case 0x0c: out += "\\f"; break;
|
|
113
|
+
case 0x0a: out += "\\n"; break;
|
|
114
|
+
case 0x0d: out += "\\r"; break;
|
|
115
|
+
case 0x09: out += "\\t"; break;
|
|
116
|
+
default:
|
|
117
|
+
out += c < 0x20 ? `\\u00${c < 0x10 ? "0" : ""}${c.toString(16)}` : value[i]!;
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
return `${out}"`;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function canonicalNumber(value: number, at: string): string {
|
|
124
|
+
if (!Number.isFinite(value)) throw new VoCanonicalError(at, "is not a finite number (NaN and Infinity have no canonical form)");
|
|
125
|
+
// ECMAScript Number::toString IS the RFC 8785 number spelling; String(-0) is "0".
|
|
126
|
+
return String(value);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function isPlainObject(value: unknown): value is Record<string, unknown> {
|
|
130
|
+
if (typeof value !== "object" || value === null || Array.isArray(value)) return false;
|
|
131
|
+
const proto = Object.getPrototypeOf(value);
|
|
132
|
+
return proto === Object.prototype || proto === null;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
function members(entries: readonly (readonly [string, string])[]): string {
|
|
136
|
+
// Keys are compared as UTF-16 code units: the default Array#sort order, as RFC 8785 requires.
|
|
137
|
+
const sorted = [...entries].sort(([a], [b]) => (a < b ? -1 : a > b ? 1 : 0));
|
|
138
|
+
return `{${sorted.map(([k, v]) => `${canonicalString(k, "member name")}:${v}`).join(",")}}`;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
/**
|
|
142
|
+
* @internal RFC 8785 canonical JSON of an UNTYPED json value (no descriptor): object members sorted,
|
|
143
|
+
* `undefined` members omitted, ECMAScript number spelling. Used where law compares plain json leaves
|
|
144
|
+
* (point guards, birth keys); typed value objects use their descriptor-driven identity instead.
|
|
145
|
+
*/
|
|
146
|
+
export function canonicalJsonValue(value: unknown, at = "value"): string {
|
|
147
|
+
if (value === null) return "null";
|
|
148
|
+
switch (typeof value) {
|
|
149
|
+
case "string": return canonicalString(value, at);
|
|
150
|
+
case "number": return canonicalNumber(value, at);
|
|
151
|
+
case "boolean": return value ? "true" : "false";
|
|
152
|
+
case "object": {
|
|
153
|
+
if (Array.isArray(value)) {
|
|
154
|
+
const out: string[] = [];
|
|
155
|
+
for (let i = 0; i < value.length; i++) {
|
|
156
|
+
if (value[i] === undefined) throw new VoCanonicalError(`${at}[${i}]`, "is undefined (a list has no absent elements)");
|
|
157
|
+
out.push(canonicalJsonValue(value[i], `${at}[${i}]`));
|
|
158
|
+
}
|
|
159
|
+
return `[${out.join(",")}]`;
|
|
160
|
+
}
|
|
161
|
+
if (!isPlainObject(value)) throw new VoCanonicalError(at, "is not a plain json object");
|
|
162
|
+
return members(Object.keys(value)
|
|
163
|
+
.filter((k) => value[k] !== undefined)
|
|
164
|
+
.map((k) => [k, canonicalJsonValue(value[k], `${at}.${k}`)] as const));
|
|
165
|
+
}
|
|
166
|
+
default:
|
|
167
|
+
throw new VoCanonicalError(at, `is a ${typeof value}, which json cannot carry`);
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
|
|
171
|
+
// ── descriptor-driven encoding ───────────────────────────────────────────────────────
|
|
172
|
+
|
|
173
|
+
const SHA256_HEX = /^[0-9a-f]{64}$/;
|
|
174
|
+
|
|
175
|
+
function canonicalField(
|
|
176
|
+
d: ValueObjectFieldDescriptor,
|
|
177
|
+
value: unknown,
|
|
178
|
+
types: ValueObjectTypes,
|
|
179
|
+
at: string,
|
|
180
|
+
): string {
|
|
181
|
+
if (value === undefined) throw new VoCanonicalError(at, "is absent, but only an optional member may be absent");
|
|
182
|
+
if (value === null) {
|
|
183
|
+
if (d.nullable === true) return "null";
|
|
184
|
+
throw new VoCanonicalError(at, "is null, but its descriptor does not declare it nullable");
|
|
185
|
+
}
|
|
186
|
+
switch (d.type) {
|
|
187
|
+
case "string": {
|
|
188
|
+
if (typeof value !== "string") throw new VoCanonicalError(at, "must be a string");
|
|
189
|
+
if (d.digest === "sha256" && !SHA256_HEX.test(value)) {
|
|
190
|
+
throw new VoCanonicalError(at, "must be a SHA-256 digest: 64 lowercase hexadecimal characters");
|
|
191
|
+
}
|
|
192
|
+
if (d.idOf !== undefined && value.length === 0) throw new VoCanonicalError(at, `must be a non-empty ${d.idOf} id`);
|
|
193
|
+
return canonicalString(value, at);
|
|
194
|
+
}
|
|
195
|
+
case "enum":
|
|
196
|
+
if (typeof value !== "string" || !d.values.includes(value)) {
|
|
197
|
+
throw new VoCanonicalError(at, `must be one of ${d.values.map((v) => `'${v}'`).join(", ")}`);
|
|
198
|
+
}
|
|
199
|
+
return canonicalString(value, at);
|
|
200
|
+
case "literal":
|
|
201
|
+
if (value !== d.value) throw new VoCanonicalError(at, `must be the literal '${d.value}'`);
|
|
202
|
+
return canonicalString(value, at);
|
|
203
|
+
case "boolean":
|
|
204
|
+
if (typeof value !== "boolean") throw new VoCanonicalError(at, "must be a boolean");
|
|
205
|
+
return value ? "true" : "false";
|
|
206
|
+
case "number":
|
|
207
|
+
if (typeof value !== "number") throw new VoCanonicalError(at, "must be a number");
|
|
208
|
+
if (d.int === true && !Number.isSafeInteger(value)) throw new VoCanonicalError(at, "must be an exact (safe) integer");
|
|
209
|
+
return canonicalNumber(value, at);
|
|
210
|
+
case "array": {
|
|
211
|
+
if (!Array.isArray(value)) throw new VoCanonicalError(at, "must be a list");
|
|
212
|
+
const out: string[] = [];
|
|
213
|
+
for (let i = 0; i < value.length; i++) out.push(canonicalField(d.items, value[i], types, `${at}[${i}]`));
|
|
214
|
+
return `[${out.join(",")}]`;
|
|
215
|
+
}
|
|
216
|
+
case "map": {
|
|
217
|
+
if (!isPlainObject(value)) throw new VoCanonicalError(at, "must be a string-keyed map (a plain object)");
|
|
218
|
+
return members(Object.keys(value).map((k) => [k, canonicalField(d.values, value[k], types, `${at}[${JSON.stringify(k)}]`)] as const));
|
|
219
|
+
}
|
|
220
|
+
case "object":
|
|
221
|
+
case "geometry": {
|
|
222
|
+
const target = types.get(d.ref);
|
|
223
|
+
if (target === undefined) throw new VoCanonicalError(at, `references the undeclared value object '${d.ref}'`);
|
|
224
|
+
return canonicalValueObject(target, value, types, at);
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function canonicalMembers(
|
|
230
|
+
fields: Readonly<Record<string, ValueObjectFieldDescriptor>>,
|
|
231
|
+
object: Record<string, unknown>,
|
|
232
|
+
types: ValueObjectTypes,
|
|
233
|
+
at: string,
|
|
234
|
+
extra: readonly (readonly [string, string])[] = [],
|
|
235
|
+
reserved: readonly string[] = [],
|
|
236
|
+
): string {
|
|
237
|
+
for (const key of Object.keys(object)) {
|
|
238
|
+
if (!(key in fields) && !reserved.includes(key) && object[key] !== undefined) {
|
|
239
|
+
throw new VoCanonicalError(`${at}.${key}`, "is not a declared member");
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
const out: (readonly [string, string])[] = [...extra];
|
|
243
|
+
for (const key of Object.keys(fields)) {
|
|
244
|
+
const member = fields[key]!;
|
|
245
|
+
const value = Object.prototype.hasOwnProperty.call(object, key) ? object[key] : undefined;
|
|
246
|
+
if (value === undefined) {
|
|
247
|
+
if (member.optional === true) continue; // ABSENT: omitted, exactly as storage omits it
|
|
248
|
+
throw new VoCanonicalError(`${at}.${key}`, "is a required member but is absent");
|
|
249
|
+
}
|
|
250
|
+
out.push([key, canonicalField(member, value, types, `${at}.${key}`)]);
|
|
251
|
+
}
|
|
252
|
+
return members(out);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
const POSITION_DEPTH: Record<GeometryKind, number> = { Point: 0, LineString: 1, MultiPoint: 1, Polygon: 2 };
|
|
256
|
+
const MIN_LENGTH: Record<GeometryKind, readonly number[]> = { Point: [], LineString: [2], MultiPoint: [0], Polygon: [0, 4] };
|
|
257
|
+
|
|
258
|
+
function canonicalCoordinates(kind: GeometryKind, value: unknown, depth: number, at: string): string {
|
|
259
|
+
if (!Array.isArray(value)) throw new VoCanonicalError(at, "must be a list");
|
|
260
|
+
if (depth === 0) {
|
|
261
|
+
if (value.length < 2 || value.length > 3) throw new VoCanonicalError(at, "must be a position: [lng, lat] or [lng, lat, altitude]");
|
|
262
|
+
return `[${value.map((n, i) => {
|
|
263
|
+
if (typeof n !== "number") throw new VoCanonicalError(`${at}[${i}]`, "must be a number");
|
|
264
|
+
return canonicalNumber(n, `${at}[${i}]`);
|
|
265
|
+
}).join(",")}]`;
|
|
266
|
+
}
|
|
267
|
+
const min = MIN_LENGTH[kind][POSITION_DEPTH[kind] - depth] ?? 0;
|
|
268
|
+
if (value.length < min) throw new VoCanonicalError(at, `must hold at least ${min} entries`);
|
|
269
|
+
return `[${value.map((v, i) => canonicalCoordinates(kind, v, depth - 1, `${at}[${i}]`)).join(",")}]`;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
/**
|
|
273
|
+
* The `nomos.vo-canonical.v1` encoding of `value` as an instance of the value object `descriptor`
|
|
274
|
+
* (nested value objects resolve through `types`). Throws {@link VoCanonicalError} when the
|
|
275
|
+
* descriptor does not admit the value.
|
|
276
|
+
*/
|
|
277
|
+
export function canonicalValueObject(
|
|
278
|
+
descriptor: ValueObjectDescriptor,
|
|
279
|
+
value: unknown,
|
|
280
|
+
types: ValueObjectTypes,
|
|
281
|
+
at: string = descriptor.name,
|
|
282
|
+
): string {
|
|
283
|
+
if (!isPlainObject(value)) throw new VoCanonicalError(at, `must be a ${descriptor.name} object`);
|
|
284
|
+
if (descriptor.fields !== undefined) return canonicalMembers(descriptor.fields, value, types, at);
|
|
285
|
+
if (descriptor.union !== undefined) {
|
|
286
|
+
const kase = value["type"];
|
|
287
|
+
if (typeof kase !== "string" || !Object.prototype.hasOwnProperty.call(descriptor.union, kase)) {
|
|
288
|
+
throw new VoCanonicalError(`${at}.type`, `must name a declared case of ${descriptor.name} (${Object.keys(descriptor.union).join(", ")})`);
|
|
289
|
+
}
|
|
290
|
+
const arm = descriptor.union[kase]!;
|
|
291
|
+
const type = [["type", canonicalString(kase, `${at}.type`)]] as const;
|
|
292
|
+
if ("ref" in arm) {
|
|
293
|
+
const target = types.get(arm.ref);
|
|
294
|
+
if (target === undefined) throw new VoCanonicalError(at, `references the undeclared value object '${arm.ref}'`);
|
|
295
|
+
return canonicalMembers(
|
|
296
|
+
{ value: { type: "object", ref: arm.ref } },
|
|
297
|
+
value, types, at, type, ["type"],
|
|
298
|
+
);
|
|
299
|
+
}
|
|
300
|
+
return canonicalMembers(arm.fields, value, types, at, type, ["type"]);
|
|
301
|
+
}
|
|
302
|
+
if (descriptor.geometry !== undefined) {
|
|
303
|
+
const kind = descriptor.geometry;
|
|
304
|
+
for (const key of Object.keys(value)) {
|
|
305
|
+
if (key !== "type" && key !== "coordinates" && value[key] !== undefined) {
|
|
306
|
+
throw new VoCanonicalError(`${at}.${key}`, "is not a member of an RFC 7946 geometry");
|
|
307
|
+
}
|
|
308
|
+
}
|
|
309
|
+
if (value["type"] !== kind) throw new VoCanonicalError(`${at}.type`, `must be '${kind}'`);
|
|
310
|
+
return members([
|
|
311
|
+
["type", canonicalString(kind, `${at}.type`)],
|
|
312
|
+
["coordinates", canonicalCoordinates(kind, value["coordinates"], POSITION_DEPTH[kind], `${at}.coordinates`)],
|
|
313
|
+
]);
|
|
314
|
+
}
|
|
315
|
+
throw new VoCanonicalError(at, `has no fields, cases or geometry in its descriptor`);
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/** The canonical encoding of a value typed by one descriptor FIELD (a list, map, or nested VO). */
|
|
319
|
+
export function canonicalDescribedValue(
|
|
320
|
+
field: ValueObjectFieldDescriptor,
|
|
321
|
+
value: unknown,
|
|
322
|
+
types: ValueObjectTypes,
|
|
323
|
+
at = "value",
|
|
324
|
+
): string {
|
|
325
|
+
return canonicalField(field, value, types, at);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** SHA-256 of `text`'s UTF-8 bytes through the ONE deterministic host port, `nomos.sha256`. */
|
|
329
|
+
function portSha256(text: string): Sha256Digest {
|
|
330
|
+
const host = (globalThis as { nomos?: { sha256?: (s: string) => string } }).nomos;
|
|
331
|
+
if (host === undefined || typeof host.sha256 !== "function") {
|
|
332
|
+
throw new Error(
|
|
333
|
+
`${VO_CANONICAL_ENCODING}: digest() needs the deterministic \`nomos.sha256\` port of the Nomos engine. ` +
|
|
334
|
+
"It runs inside .plan() and invariant bodies; a test installs globalThis.nomos.sha256.",
|
|
335
|
+
);
|
|
336
|
+
}
|
|
337
|
+
const digest = host.sha256(text);
|
|
338
|
+
if (typeof digest !== "string" || !SHA256_HEX.test(digest)) {
|
|
339
|
+
throw new Error(`${VO_CANONICAL_ENCODING}: the nomos.sha256 port returned a malformed digest`);
|
|
340
|
+
}
|
|
341
|
+
return digest as Sha256Digest;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
/** Build the identity operations over one canonical-encoding function. */
|
|
345
|
+
export function identityOf<T>(encode: (value: unknown) => string): ValueIdentity<T> {
|
|
346
|
+
return {
|
|
347
|
+
encoding: VO_CANONICAL_ENCODING,
|
|
348
|
+
canonical: (value: T) => encode(value),
|
|
349
|
+
equals(a: T | undefined, b: T | undefined): boolean {
|
|
350
|
+
if (a === undefined || b === undefined) return a === b;
|
|
351
|
+
return encode(a) === encode(b);
|
|
352
|
+
},
|
|
353
|
+
digest: (value: T) => portSha256(encode(value)),
|
|
354
|
+
};
|
|
355
|
+
}
|
package/src/value_object.ts
CHANGED
|
@@ -66,7 +66,14 @@ import {
|
|
|
66
66
|
type ValueObjectDescriptor,
|
|
67
67
|
type ValueObjectFieldDescriptor,
|
|
68
68
|
type ValueObjectFieldMeta,
|
|
69
|
+
type TypedId,
|
|
69
70
|
} from "./fields.js";
|
|
71
|
+
import {
|
|
72
|
+
canonicalDescribedValue,
|
|
73
|
+
canonicalValueObject,
|
|
74
|
+
identityOf,
|
|
75
|
+
type ValueIdentity,
|
|
76
|
+
} from "./value_identity.js";
|
|
70
77
|
// LOCAL zod introspection (the same `_def ?? def` convention as codegen_ts.ts) —
|
|
71
78
|
// deliberately NOT imported from codegen_ts.js: this module rides the RUNTIME barrel
|
|
72
79
|
// (the engine lump bundles it, `--platform=neutral`), and codegen_ts transitively
|
|
@@ -131,7 +138,7 @@ export interface OptionalValueObject<T = unknown> {
|
|
|
131
138
|
* Carries the zod (validation + TS inference), the canonical descriptor (the IR/codegen
|
|
132
139
|
* shape), and lowers onto an aggregate via {@link ValueObject.field}.
|
|
133
140
|
*/
|
|
134
|
-
export interface ValueObject<T = unknown> {
|
|
141
|
+
export interface ValueObject<T = unknown> extends ValueIdentity<T> {
|
|
135
142
|
readonly __isValueObject: true;
|
|
136
143
|
/** The declared name — the generated TS interface / Dart class name. */
|
|
137
144
|
readonly name: string;
|
|
@@ -167,6 +174,10 @@ export interface ValueObject<T = unknown> {
|
|
|
167
174
|
merge(driver: Driver): Field<T, "json">;
|
|
168
175
|
/** Phantom to keep T load-bearing for inference. */
|
|
169
176
|
readonly _t?: T;
|
|
177
|
+
// VALUE IDENTITY (CAP-853, `value_identity.ts`): `canonical(v)` is the `nomos.vo-canonical.v1`
|
|
178
|
+
// encoding (RFC 8785 over the descriptor-judged value), `equals(a, b)` compares encodings,
|
|
179
|
+
// `digest(v)` is its SHA-256 through the engine's deterministic port. Use them in `.plan()` and
|
|
180
|
+
// invariant bodies instead of `JSON.stringify`, whose bytes depend on key order.
|
|
170
181
|
}
|
|
171
182
|
|
|
172
183
|
/** Infer the authored TS type of a ValueObject. */
|
|
@@ -434,14 +445,16 @@ export const NOMOS_BRANDED_ID_BRAND: unique symbol = Symbol.for("nomos.brandedId
|
|
|
434
445
|
|
|
435
446
|
const BRAND_NAME_RE = /^[A-Z][A-Za-z0-9_]*$/;
|
|
436
447
|
|
|
437
|
-
/** Declare a NOMINAL string id: `siteId: brandedId("SiteId")`.
|
|
438
|
-
|
|
448
|
+
/** Declare a NOMINAL string id: `siteId: brandedId("SiteId")`. The parsed value is typed
|
|
449
|
+
* {@link TypedId}`<"SiteId">` in law code too (the same type a `t.brandedId("SiteId")` field holds), so a
|
|
450
|
+
* plain string is a compile error on both sides of the wire. */
|
|
451
|
+
export function brandedId<const Name extends string>(name: Name): z.ZodType<TypedId<Name>> {
|
|
439
452
|
if (!BRAND_NAME_RE.test(name)) {
|
|
440
453
|
throw new Error(`brandedId: '${name}' must be a capitalized identifier — it becomes the generated branded type's name.`);
|
|
441
454
|
}
|
|
442
455
|
const schema = z.string();
|
|
443
456
|
Object.defineProperty(schema, NOMOS_BRANDED_ID_BRAND, { value: name, enumerable: false });
|
|
444
|
-
return schema
|
|
457
|
+
return schema as unknown as z.ZodType<TypedId<Name>>;
|
|
445
458
|
}
|
|
446
459
|
|
|
447
460
|
/** Resolve the `brandedId(...)` brand name of a schema (through optional/default/nullable). */
|
|
@@ -645,6 +658,20 @@ export function voMetaOf(zt: z.ZodTypeAny): ValueObjectFieldMeta | undefined {
|
|
|
645
658
|
return undefined;
|
|
646
659
|
}
|
|
647
660
|
|
|
661
|
+
/**
|
|
662
|
+
* VALUE IDENTITY for a constrained schema built FROM value objects — `valueIdentity(z.array(Site))`,
|
|
663
|
+
* `valueIdentity(z.record(z.string(), Reading))`, `valueIdentity(Site.zod.optional())` — the same
|
|
664
|
+
* `nomos.vo-canonical.v1` encoding a value object's own `canonical` / `equals` / `digest` use, derived
|
|
665
|
+
* from the same descriptor rules (an out-of-set schema throws, exactly as in `valueObject(...)`).
|
|
666
|
+
* Declare it at module scope and call it inside `.plan()` / invariant bodies.
|
|
667
|
+
*/
|
|
668
|
+
export function valueIdentity<T>(schema: z.ZodType<T> | ValueObject<T>): ValueIdentity<T> {
|
|
669
|
+
if (isValueObject(schema)) return schema as ValueObject<T>;
|
|
670
|
+
const deps = new Map<string, ValueObjectDescriptor>();
|
|
671
|
+
const field = describeZod("valueIdentity", "value", schema as z.ZodTypeAny, deps);
|
|
672
|
+
return identityOf<T>((value) => canonicalDescribedValue(field, value, deps));
|
|
673
|
+
}
|
|
674
|
+
|
|
648
675
|
/** The blessed lawful sample per RFC 7946 geometry kind (proof synthesis + docs). */
|
|
649
676
|
const GEOMETRY_SAMPLES: Record<GeometryKind, unknown> = {
|
|
650
677
|
Point: { type: "Point", coordinates: [0, 0] },
|
|
@@ -764,7 +791,11 @@ function makeValueObject<T>(
|
|
|
764
791
|
// serialized directly. voMetaOf() is the one resolver every introspector uses; aggregate
|
|
765
792
|
// fields separately copy the canonical descriptor into their stable value contract.
|
|
766
793
|
Object.defineProperty(zodSchema, NOMOS_VO_BRAND, { value: meta, enumerable: false });
|
|
794
|
+
const types = new Map<string, ValueObjectDescriptor>(
|
|
795
|
+
[descriptor, ...dependencies].map((d) => [d.name, d] as const),
|
|
796
|
+
);
|
|
767
797
|
const vo: ValueObject<T> = {
|
|
798
|
+
...identityOf<T>((value) => canonicalValueObject(descriptor, value, types)),
|
|
768
799
|
__isValueObject: true,
|
|
769
800
|
name,
|
|
770
801
|
zod: zodSchema,
|
package/src/wire.ts
CHANGED
|
@@ -211,6 +211,8 @@ export interface WireIntent {
|
|
|
211
211
|
workspace: string;
|
|
212
212
|
genesisChain: unknown;
|
|
213
213
|
parentWorkspaceRoles?: readonly import("./births.js").WorkspaceRole[];
|
|
214
|
+
/** The typed key the workspace name hashes (business births under typed-key law → `nomos:birthKey`). */
|
|
215
|
+
birthKey?: string;
|
|
214
216
|
}[];
|
|
215
217
|
/** Exact target calls authored by source law. The OpenUSD bytes come from a compiler-generated
|
|
216
218
|
* encoder for the target Directive object; no public operation string or generic payload crosses. */
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
// NOMOS — The one spelling of a nested operation field's ledger label.
|
|
2
|
+
//
|
|
3
|
+
// A top-level payload field is ledgered under its own name. A field INSIDE a structural value
|
|
4
|
+
// (a record member, a map value, a list element, a union variant) is ledgered under a path built
|
|
5
|
+
// from these segments, so its transport allocation survives a change elsewhere in the structure
|
|
6
|
+
// (the 1 October 2026 `canvasRequirements` incident: nested `isRequired` keys moved because their
|
|
7
|
+
// structural sids moved). law_graph.ts allocates by these paths; baseline_export.ts recovers the
|
|
8
|
+
// same paths from released law so the two agree without a sidecar.
|
|
9
|
+
|
|
10
|
+
/** A record member beneath `parent`. */
|
|
11
|
+
export const nestedMemberPath = (parent: string, name: string): string => `${parent}.${name}`;
|
|
12
|
+
/** The element of a list beneath `parent`. */
|
|
13
|
+
export const listElementPath = (parent: string): string => `${parent}[]`;
|
|
14
|
+
/** The value of a map beneath `parent`. */
|
|
15
|
+
export const mapValuePath = (parent: string): string => `${parent}{}`;
|
|
16
|
+
/** One variant of a union beneath `parent`, by its discriminator label or ordinal. */
|
|
17
|
+
export const unionVariantPath = (parent: string, variant: string): string => `${parent}|${variant}|`;
|
package/src/workspace_routing.ts
CHANGED
|
@@ -164,7 +164,7 @@ export function routeTagHexOfMintedId(id: string): string | undefined {
|
|
|
164
164
|
// ── the plan trace (the mint-front-door family, aimed at HOMES) ──────────────────────
|
|
165
165
|
|
|
166
166
|
/** Build a schema-valid probe payload: every top-level string field gets its sentinel. */
|
|
167
|
-
function buildProbePayload(
|
|
167
|
+
export function buildProbePayload(
|
|
168
168
|
schema: z.ZodTypeAny,
|
|
169
169
|
sentinelOf: Map<string, string>,
|
|
170
170
|
jsonSafe = false,
|