@githolon/dsl 0.100.5 → 0.101.0

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 (97) hide show
  1. package/dart/CHANGELOG.md +12 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +220 -71
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/acquire_session_credentials_test.dart +1 -1
  8. package/dart/test/adv_realm_interference_test.dart +1 -1
  9. package/dart/test/bundle_version_pairing_test.dart +11 -22
  10. package/dart/test/cluster_attested_test.dart +1 -1
  11. package/dart/test/cluster_births_test.dart +1 -1
  12. package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
  13. package/dart/test/cluster_local_autosync_test.dart +1 -1
  14. package/dart/test/cluster_scale_test.dart +1 -1
  15. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  16. package/dart/test/dlq_session_surface_test.dart +1 -1
  17. package/dart/test/errors_test.dart +63 -12
  18. package/dart/test/first_app_gauntlet_test.dart +132 -87
  19. package/dart/test/local_sync_inert_test.dart +10 -5
  20. package/dart/test/parent_signer_recovery_test.dart +1 -1
  21. package/dart/test/sealing_client_test.dart +1 -1
  22. package/dart/test/session_bound_test.dart +1 -1
  23. package/dart/test/test_harness_test.dart +2 -2
  24. package/dart/test/vo_read_typing_test.dart +1 -1
  25. package/dart/test/watch_after_offer_stall_test.dart +1 -1
  26. package/dart/test/webview_bridge_test.dart +274 -0
  27. package/dart_types/CHANGELOG.md +6 -0
  28. package/dart_types/lib/src/errors.dart +136 -30
  29. package/dart_types/lib/src/surfaced.dart +48 -2
  30. package/dart_types/lib/src/wire.dart +3 -1
  31. package/dart_types/pubspec.yaml +1 -1
  32. package/dart_types/test/surfaced_test.dart +19 -0
  33. package/package.json +1 -1
  34. package/src/activity.ts +43 -4
  35. package/src/aggregate.ts +132 -22
  36. package/src/attested_core.ts +1 -1
  37. package/src/authoring.ts +119 -6
  38. package/src/birth_meter.ts +4 -4
  39. package/src/birth_meter_ensure.ts +165 -29
  40. package/src/births.ts +53 -22
  41. package/src/build_package.ts +96 -20
  42. package/src/business_birth.ts +79 -16
  43. package/src/codegen_dart.ts +267 -37
  44. package/src/codegen_dart_client.ts +930 -87
  45. package/src/codegen_openapi.ts +28 -5
  46. package/src/codegen_proof.ts +107 -11
  47. package/src/codegen_ts.ts +961 -135
  48. package/src/compile_package_main.ts +172 -53
  49. package/src/conflicts.ts +252 -0
  50. package/src/describe_model.ts +448 -0
  51. package/src/directive.ts +5 -38
  52. package/src/drivers.ts +12 -4
  53. package/src/engine_entry.ts +4 -0
  54. package/src/evolve_preview.ts +72 -11
  55. package/src/fields.ts +201 -55
  56. package/src/framework/application_home.ts +103 -0
  57. package/src/framework/application_home_domain.ts +2 -0
  58. package/src/framework/attested_receipts.ts +2 -2
  59. package/src/framework/auth_provider_law.ts +51 -24
  60. package/src/framework/business_corrections.ts +170 -0
  61. package/src/framework/disclosure.ts +1 -1
  62. package/src/framework/domain_lifecycle.ts +250 -16
  63. package/src/framework/encryption_identity.ts +1 -1
  64. package/src/framework/encryption_scope.ts +21 -13
  65. package/src/framework/era_certification.ts +1 -1
  66. package/src/framework/framework_aggregates.ts +2 -0
  67. package/src/framework/genesis.ts +1 -1
  68. package/src/framework/home.ts +22 -9
  69. package/src/framework/identity.ts +5 -5
  70. package/src/framework/impure_capability.ts +2 -2
  71. package/src/framework/owned_custody.ts +561 -0
  72. package/src/framework/platform_admin.ts +1 -1
  73. package/src/framework/relations.ts +9 -1
  74. package/src/framework/repair.ts +1 -1
  75. package/src/framework/signing_identity.ts +2 -2
  76. package/src/framework/source_roster.ts +1 -1
  77. package/src/framework/workspaces.ts +17 -10
  78. package/src/generated_client_names.ts +39 -0
  79. package/src/index.ts +20 -15
  80. package/src/kernel_capabilities.ts +25 -1
  81. package/src/law_check.ts +4 -1
  82. package/src/manifest.ts +356 -63
  83. package/src/read_projection_manifest.ts +8 -0
  84. package/src/read_projection_namespace.ts +4 -0
  85. package/src/relation.ts +1 -1
  86. package/src/shape_compat.ts +61 -5
  87. package/src/stable_ids.ts +38 -6
  88. package/src/stable_ids_types.ts +30 -0
  89. package/src/surfaced_lint.ts +0 -0
  90. package/src/usd.ts +62 -11
  91. package/src/usd_layers.ts +54 -3
  92. package/src/value_object.ts +23 -19
  93. package/src/wire.ts +1 -0
  94. package/src/wire_encode.ts +48 -4
  95. package/src/workspace_routing.ts +57 -0
  96. package/src/workspace_sharding.ts +9 -9
  97. package/src/certified_read.ts +0 -313
@@ -63,6 +63,46 @@ export interface DomainInterface {
63
63
  >;
64
64
  }
65
65
 
66
+ /** Remove framework-managed aggregate fields from the client-facing structural interface. Stable ids for
67
+ * those fields remain in the law/lockfile; clients negotiate only the business shape they can see. */
68
+ export function publicClientInterface(
69
+ modules: readonly DomainModule[],
70
+ offered: Readonly<Record<string, DomainInterface>>,
71
+ ): Record<string, DomainInterface> {
72
+ const hidden = new Map<string, Map<string, Set<string>>>();
73
+ for (const mod of modules) {
74
+ const domain = mod.domain ?? mod.name;
75
+ const byAggregate = hidden.get(domain) ?? new Map<string, Set<string>>();
76
+ hidden.set(domain, byAggregate);
77
+ for (const aggregate of mod.aggregates ?? []) {
78
+ const fields = (aggregate as { internalFields?: readonly string[] }).internalFields ?? [];
79
+ if (fields.length > 0) byAggregate.set(aggregate.id, new Set(fields));
80
+ }
81
+ }
82
+ const result: Record<string, DomainInterface> = {};
83
+ for (const [domain, value] of Object.entries(offered)) {
84
+ const byAggregate = hidden.get(domain);
85
+ if (byAggregate === undefined || byAggregate.size === 0) {
86
+ result[domain] = value;
87
+ continue;
88
+ }
89
+ const aggregates: Record<string, AggregateReadShape> = {};
90
+ for (const [sid, aggregate] of Object.entries(value.aggregates)) {
91
+ const fields = byAggregate.get(aggregate.label);
92
+ aggregates[sid] = fields === undefined
93
+ ? aggregate
94
+ : {
95
+ ...aggregate,
96
+ fields: Object.fromEntries(
97
+ Object.entries(aggregate.fields).filter(([, field]) => !fields.has(field.label)),
98
+ ),
99
+ };
100
+ }
101
+ result[domain] = { ...value, aggregates };
102
+ }
103
+ return result;
104
+ }
105
+
66
106
  /** The verdict: compatible iff nothing is missing and nothing is type-incompatible.
67
107
  * `missing` = a depended-on directive/aggregate/field the workspace doesn't expose;
68
108
  * `incompatible` = present but type-divergent (a payload field kind, a retyped read field). */
@@ -123,6 +163,10 @@ function aggFieldReadKind(f: Field): string {
123
163
  * stable-id surface. Without stable ids (pre-#58) aggregate fields key by NAME and the
124
164
  * kind comes from the field declaration — a graceful, rename-unaware fallback. */
125
165
  export function domainInterface(mod: DomainModule, stableIds?: StableIds): DomainInterface {
166
+ const isInternal = (value: unknown): boolean =>
167
+ !!value &&
168
+ typeof value === "object" &&
169
+ (value as { businessBirthInternal?: unknown }).businessBirthInternal === true;
126
170
  const enumTypes = new Map<string, string>();
127
171
  // DETERMINISTIC KEY ORDER — directive ids, aggregate sids, and field keys are SORTED, so the
128
172
  // emitted interface is byte-stable and the layered-USD fold (which merges + sorts) reproduces it
@@ -134,27 +178,38 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
134
178
  };
135
179
  const directives: Record<string, DirectivePayloadShape> = {};
136
180
  const dirEntries: Array<[string, DirectivePayloadShape]> = [];
137
- for (const d of (mod.directives ?? []) as Array<{ id?: unknown; payloadSchema?: unknown }>) {
181
+ for (const d of (mod.directives ?? []) as Array<{
182
+ id?: unknown;
183
+ payloadSchema?: unknown;
184
+ businessBirth?: { publicPayloadSchema?: unknown };
185
+ }>) {
138
186
  if (!d || typeof d.id !== "string") continue;
187
+ if (isInternal(d)) continue;
139
188
  const fieldEntries: Array<[string, PayloadFieldShape]> = [];
140
- const schema = d.payloadSchema;
189
+ const schema = d.businessBirth?.publicPayloadSchema ?? d.payloadSchema;
141
190
  if (schema && typeof schema === "object") {
142
191
  for (const spec of introspectPayload(schema as z.ZodTypeAny, enumTypes)) {
143
192
  fieldEntries.push([spec.name, { kind: payloadFieldKind(spec), optional: !!spec.optional }]);
144
193
  }
145
194
  }
146
- dirEntries.push([d.id, { fields: sortedRec(fieldEntries) }]);
195
+ dirEntries.push([d.businessBirth === undefined ? d.id : "birth", { fields: sortedRec(fieldEntries) }]);
147
196
  }
148
197
  Object.assign(directives, sortedRec(dirEntries));
149
198
  const aggregates: Record<string, AggregateReadShape> = {};
150
199
  const aggEntries: Array<[string, AggregateReadShape]> = [];
151
- for (const agg of (mod.aggregates ?? []) as Array<{ id: string; fields: Record<string, Field> }>) {
200
+ for (const agg of (mod.aggregates ?? []) as Array<{
201
+ id: string;
202
+ fields: Record<string, Field>;
203
+ internalFields?: readonly string[];
204
+ }>) {
205
+ if (isInternal(agg)) continue;
152
206
  const label = agg.id;
153
207
  const sa = stableIds?.aggregates?.[label];
154
208
  const aggSid = sa?.sid ?? label;
155
209
  const fieldEntries: Array<[string, { kind: string; label: string }]> = [];
210
+ const internal = new Set(agg.internalFields ?? []);
156
211
  for (const [fname, f] of Object.entries(agg.fields ?? {})) {
157
- if (!f || f.kind === "hasMany") continue;
212
+ if (!f || f.kind === "hasMany" || internal.has(fname)) continue;
158
213
  const fsid = sa?.fields?.[fname]?.sid ?? fname;
159
214
  const kind = sa?.fields?.[fname]?.kind ?? aggFieldReadKind(f);
160
215
  fieldEntries.push([fsid, { kind, label: fname }]);
@@ -168,6 +223,7 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
168
223
  const foreignReadEntries: Array<[string, ForeignEdge[]]> = [];
169
224
  for (const d of (mod.directives ?? []) as Array<{ id?: unknown; declaredForeignReads?: ForeignEdge[] }>) {
170
225
  if (!d || typeof d.id !== "string") continue;
226
+ if (isInternal(d)) continue;
171
227
  if (d.declaredForeignReads !== undefined && d.declaredForeignReads.length > 0) {
172
228
  foreignReadEntries.push([d.id, d.declaredForeignReads.map((e) => ({ ...e }))]);
173
229
  }
package/src/stable_ids.ts CHANGED
@@ -27,6 +27,11 @@
27
27
  * Note the NESTING: field sids mint under the AGGREGATE SID (not its name), so a
28
28
  * renamed aggregate's fields keep their identities for free.
29
29
  *
30
+ * GLOBAL FRAMEWORK AGGREGATES are the one exception to domain-scoped minting. The same
31
+ * kernel-owned aggregate (for example the relation spine) is composed into several
32
+ * independently compiled domains, so it mints beneath a reserved global scope. Every
33
+ * package therefore publishes one identical field-contract identity for that shared row.
34
+ *
30
35
  * CONTINUITY (across compiles): `nomos-compile` reads the PRIOR build's identity
31
36
  * manifests / the committed `nomos.stable-ids.json` lockfile and derives a continuity
32
37
  * table per domain — match by name first; then the UNAMBIGUOUS-PAIR rule: exactly ONE
@@ -48,7 +53,13 @@ import { createHash } from "node:crypto";
48
53
  import type { WireSchema } from "./wire.js";
49
54
  import type { StableAggregateId, StableFieldId, StableIds } from "./stable_ids_types.js";
50
55
 
51
- export type { StableAggregateId, StableFieldId, StableIds } from "./stable_ids_types.js";
56
+ export type {
57
+ StableAggregateId,
58
+ StableFieldEvolution,
59
+ StableFieldId,
60
+ StableFieldValueKind,
61
+ StableIds,
62
+ } from "./stable_ids_types.js";
52
63
 
53
64
  /** Mint one stable id: first 16 hex chars of sha256 over the versioned scope. */
54
65
  export function mintStableId(scope: string): string {
@@ -67,6 +78,11 @@ export function mintFieldSid(aggSid: string, fieldName: string): string {
67
78
  return mintStableId(`field:${aggSid}:${fieldName}`);
68
79
  }
69
80
 
81
+ /** Canonical scope for a framework aggregate shared by independently compiled domains. */
82
+ export function mintGlobalAggregateSid(aggName: string): string {
83
+ return mintStableId(`global-aggregate:${aggName}`);
84
+ }
85
+
70
86
  /** One inferred rename, printed by the compile summary (visibility for the silent lane). */
71
87
  export interface InferredRename {
72
88
  /** `"<Agg>"` for an aggregate rename, `"<Agg>.<field>"` for a field rename. */
@@ -93,7 +109,13 @@ export interface InferredRename {
93
109
  */
94
110
  export function deriveStableIdContinuity(
95
111
  domainName: string,
96
- currentAggregates: { id: string; schema: WireSchema; kinds?: Record<string, string> }[],
112
+ currentAggregates: {
113
+ id: string;
114
+ schema: WireSchema;
115
+ kinds?: Record<string, string>;
116
+ /** The same framework-owned aggregate is composed into more than one domain. */
117
+ global?: boolean;
118
+ }[],
97
119
  prior: StableIds | undefined,
98
120
  /** The prior canonical manifest's aggregate schemas (keyed by PRIOR name) — when
99
121
  * supplied, tightens the field-pair rule to same-driver only. */
@@ -111,6 +133,10 @@ export function deriveStableIdContinuity(
111
133
  { sid: string; was?: string[]; priorFields?: Record<string, StableFieldId>; priorSchema?: WireSchema }
112
134
  > = {};
113
135
  for (const agg of currentAggregates) {
136
+ if (agg.global === true) {
137
+ resolved[agg.id] = { sid: mintGlobalAggregateSid(agg.id), priorFields: {} };
138
+ continue;
139
+ }
114
140
  const p = priorAggs[agg.id];
115
141
  if (p !== undefined) {
116
142
  resolved[agg.id] = {
@@ -148,14 +174,20 @@ export function deriveStableIdContinuity(
148
174
  const aggregates: Record<string, StableAggregateId> = {};
149
175
  for (const agg of currentAggregates) {
150
176
  const r = resolved[agg.id];
151
- const aggSid = r?.sid ?? mintAggregateSid(domainSid, agg.id);
152
- const priorFields = r?.priorFields ?? {};
177
+ const aggSid =
178
+ agg.global === true
179
+ ? mintGlobalAggregateSid(agg.id)
180
+ : r?.sid ?? mintAggregateSid(domainSid, agg.id);
181
+ const priorFields = agg.global === true ? {} : r?.priorFields ?? {};
153
182
  const fieldNames = Object.keys(agg.schema);
154
183
  const fields: Record<string, StableFieldId> = {};
155
184
  for (const f of fieldNames) {
156
185
  const pf = priorFields[f];
157
186
  if (pf !== undefined) {
158
- fields[f] = { sid: pf.sid, ...(pf.was !== undefined ? { was: pf.was } : {}) };
187
+ // Carry compiler-owned contract/evolution history into the continuity input. The
188
+ // final manifest re-derives the current contract, but needs this prior descriptor to
189
+ // prove automatic widening and retain routes from every still-supported era.
190
+ fields[f] = { ...pf };
159
191
  }
160
192
  }
161
193
  const fDisappeared = Object.keys(priorFields).filter((n) => !fieldNames.includes(n));
@@ -179,7 +211,7 @@ export function deriveStableIdContinuity(
179
211
  if (candidates.length === 1) {
180
212
  const newF = candidates[0]!;
181
213
  const pf = priorFields[oldF]!;
182
- fields[newF] = { sid: pf.sid, was: [...(pf.was ?? []), oldF] };
214
+ fields[newF] = { ...pf, was: [...(pf.was ?? []), oldF] };
183
215
  inferred.push({ what: `${agg.id}.${newF}`, from: oldF, to: newF, sid: pf.sid });
184
216
  }
185
217
  }
@@ -11,6 +11,22 @@
11
11
  * (build-time only — `nomos-compile` + `@githolon/dsl/manifest`).
12
12
  */
13
13
 
14
+ /** The read decoder needed to interpret one evolution rule's source value. */
15
+ export interface StableFieldValueKind {
16
+ readonly kind: string;
17
+ readonly mapValueKind?: string;
18
+ }
19
+
20
+ /** A deterministic source-contract → target-contract projection. `identity` is a
21
+ * compiler-proven widening; `cases` is an exhaustive finite business mapping. */
22
+ export interface StableFieldEvolution {
23
+ readonly from: string;
24
+ readonly to: string;
25
+ readonly source: StableFieldValueKind;
26
+ readonly mode: "identity" | "cases";
27
+ readonly cases?: readonly { readonly from: unknown; readonly to: unknown }[];
28
+ }
29
+
14
30
  /** One field's stable identity: the minted sid + the labels it previously wore. */
15
31
  export interface StableFieldId {
16
32
  readonly sid: string;
@@ -21,6 +37,20 @@ export interface StableFieldId {
21
37
  * Always set at emission; continuity input may omit it (the current module is the
22
38
  * source of the CURRENT kind). */
23
39
  readonly kind?: string;
40
+ /** Content identity of the COMPLETE generated business-value contract for this field
41
+ * (canonical Zod JSON schema plus the field metadata that shapes TS/Dart decoding).
42
+ * Conflict observations use it to distinguish an unrelated law rollout from an actual
43
+ * value-shape change; it is compiler-derived, never tenant-authored. */
44
+ readonly contract?: string;
45
+ /** Canonical descriptor hashed by `contract`. Retained so the next compiler can prove
46
+ * structural identity widening without asking the tenant to restate it. */
47
+ readonly shape?: unknown;
48
+ /** Complete stable schema identity recorded on conflict observations. */
49
+ readonly schema?: string;
50
+ /** Current kernel merge driver, retained for safe automatic evolution comparison. */
51
+ readonly driver?: unknown;
52
+ /** Every still-supported route, accumulated across releases. */
53
+ readonly evolutions?: readonly StableFieldEvolution[];
24
54
  /** BIRTH-PRESENCE LAW (`.fromBirth()`): recorded at the field's first fromBirth emission.
25
55
  * The compile guard refuses NEWLY declaring fromBirth on a sid that existed without it
26
56
  * (old rows may lack the field — the non-nullable read type would lie). */
Binary file
package/src/usd.ts CHANGED
@@ -33,7 +33,11 @@ import {
33
33
  type CanonicalManifest,
34
34
  type CanonicalProjectionReturn,
35
35
  } from "./manifest.js";
36
- import type { AggregateHandle } from "./aggregate.js";
36
+ import {
37
+ OWNED_KEY_RELATION,
38
+ OWNED_READ_RELATION,
39
+ type AggregateHandle,
40
+ } from "./aggregate.js";
37
41
  import {
38
42
  mergeRequiresScope,
39
43
  CompositionViolation,
@@ -54,6 +58,7 @@ import {
54
58
  } from "./read_projection_namespace.js";
55
59
  import { qualifyManifestAggregateLabels } from "./read_projection_manifest.js";
56
60
  import { guardSharedReadProjectionKeys } from "./read_projection_composition.js";
61
+ import { OWNED_RECOVERY_RELATION } from "./framework/owned_custody.js";
57
62
 
58
63
  /** The referential marker of a directive prim (kernel `RefMode` wire string). */
59
64
  export type RefMode = WireRefMode;
@@ -552,7 +557,7 @@ export interface UsdLayer {
552
557
  readonly nomosReadVisibility?: {
553
558
  readonly v: 1;
554
559
  /** Aggregate TYPE name → the capability a read touching it requires. SORTED by type name. */
555
- readonly aggregates: Record<string, { requires: string }>;
560
+ readonly aggregates: Record<string, { requires: string; ownerField?: string }>;
556
561
  };
557
562
  /**
558
563
  * THE ENCRYPTION GATE DECLARATION (e2e_encryption.md slice 2). Present IFF ≥1 field of
@@ -567,7 +572,7 @@ export interface UsdLayer {
567
572
  */
568
573
  readonly nomosEncryption?: {
569
574
  readonly v: 1;
570
- readonly aggregates: Record<string, { scope: string; fields: string[]; delegatable: false }>;
575
+ readonly aggregates: Record<string, { scope: string; fields: string[]; delegatable: false; owned?: true; ownerField?: string }>;
571
576
  };
572
577
  /**
573
578
  * THE CAPABILITY DECLARATION IN LAW (Full-A — capability_marketplace.md §4): the impure-
@@ -975,9 +980,9 @@ function synthesizeReadRelationTypes(
975
980
  ): import("./relation_schema.js").RelationTypeDecl[] {
976
981
  const caps = new Set<string>();
977
982
  for (const a of manifest.aggregates) {
978
- if (a.requiresRead !== undefined) caps.add(a.requiresRead);
983
+ if (a.requiresRead !== undefined && a.owned === undefined) caps.add(a.requiresRead);
979
984
  }
980
- if (caps.size === 0) return [];
985
+ const out: import("./relation_schema.js").RelationTypeDecl[] = [];
981
986
  // GLOBAL (un-namespaced): the read gate hardcodes `ObjectRef::new("workspace","self")` in Rust
982
987
  // (`wasm_git_holon.rs`), so the synthesized read-visibility `workspace` type MUST stay bare — it is
983
988
  // NOT namespaced by the owning domain (site-6 exemption; a Rust change would be needed to qualify it).
@@ -989,7 +994,26 @@ function synthesizeReadRelationTypes(
989
994
  for (const cap of [...caps].sort()) {
990
995
  builder.relation(cap, { allow: [user(), key(), anyUser()] });
991
996
  }
992
- return [builder];
997
+ if (caps.size > 0) out.push(builder);
998
+ // `.owned()` is record-grained: its read relation lives on the aggregate object itself, so the
999
+ // existing row filter checks `<qualified-type>:<record-id>#<relation>`. The type remains local and is
1000
+ // therefore qualified by the same domain key as the aggregate; only direct user principals may sit it.
1001
+ for (const a of [...manifest.aggregates].filter((x) => x.owned !== undefined).sort((x, y) => x.id.localeCompare(y.id))) {
1002
+ const owned = a.owned!;
1003
+ // `manifest` already carries the read-projection-qualified type (`domain.Note`). Keep that exact
1004
+ // spelling global here rather than applying the relation namespace (`domain__Note`): the read gate
1005
+ // constructs its per-record object from the projected row type, so schema, tuple and row must agree.
1006
+ out.push(relationType(a.id).asGlobal().relation(owned.relation, { allow: [user()] }));
1007
+ }
1008
+ if (manifest.aggregates.some((x) => x.owned !== undefined)) {
1009
+ out.push(
1010
+ relationType("nomos-owned-principal")
1011
+ .asGlobal()
1012
+ .relation(OWNED_KEY_RELATION, { allow: [key()] })
1013
+ .relation(OWNED_RECOVERY_RELATION, { allow: [user()] }),
1014
+ );
1015
+ }
1016
+ return out;
993
1017
  }
994
1018
 
995
1019
  /**
@@ -1021,6 +1045,21 @@ export function emitUsd(
1021
1045
  const qaType = (n: string): string =>
1022
1046
  namespacedAggs.has(n) ? qualifyReadProjectionType(rpDomainKey, n) : n;
1023
1047
  const manifest = qualifyManifestAggregateLabels(bareManifest, rpDomainKey, namespacedAggs);
1048
+ // `.owned()` reserves its deliberately obscure relation spelling. If tenant law nevertheless reaches
1049
+ // for it, fail here instead of merging, qualifying or shadowing the declaration: a collision is useful
1050
+ // evidence that the tenant is modelling the same concept, but silently capturing it would make their
1051
+ // code mean something they did not write.
1052
+ const ownedRelations = new Set([OWNED_READ_RELATION, OWNED_KEY_RELATION, OWNED_RECOVERY_RELATION]);
1053
+ for (const d of l.module.relationTypes ?? []) {
1054
+ for (const relation of d.relations.keys()) {
1055
+ if (!ownedRelations.has(relation)) continue;
1056
+ throw new Error(
1057
+ `relationType(${JSON.stringify(d.type)}): relation ${JSON.stringify(relation)} is reserved by ` +
1058
+ `Nomos .owned(). Sorry mate, that concept is already taken by the framework; give the ` +
1059
+ `business relation a different name.`,
1060
+ );
1061
+ }
1062
+ }
1024
1063
  // THE READ-PROJECTION NAMESPACE ERA KEY: present iff ≥1 aggregate was qualified (the witness
1025
1064
  // marker — mirrors `nomosRelationNamespace`; the qualification itself is baked into the keys).
1026
1065
  const nomosReadProjectionNamespace =
@@ -1053,7 +1092,9 @@ export function emitUsd(
1053
1092
  // must be qualified. A `global` type (governance `workspace`, signer `identity`, read-visibility
1054
1093
  // `workspace`) stays bare on BOTH the schema and the gate side, so it is excluded here.
1055
1094
  const namespacedTypes = new Set(
1056
- (l.module.relationTypes ?? []).filter((d) => d.global !== true).map((d) => d.type),
1095
+ [...(l.module.relationTypes ?? []), ...readRelationTypes]
1096
+ .filter((d) => d.global !== true)
1097
+ .map((d) => d.type),
1057
1098
  );
1058
1099
  // THE AMBIGUITY GUARD (relation-namespacing): a bare `writeTuple({object:"workspace:self", relation:R})`
1059
1100
  // is auto-qualified iff `(type,R)` is a LOCAL non-global authored relation, and left BARE iff it is a
@@ -1388,20 +1429,30 @@ export function emitUsd(
1388
1429
  // THE READ-VISIBILITY GATE (slice 5.1): every aggregate the manifest carries a
1389
1430
  // `requiresRead` for, keyed by TYPE name → required cap. SORTED keys
1390
1431
  // (byte-determinism); omit-when-empty so an all-public layer is byte-identical.
1391
- const visAggregates: Record<string, { requires: string }> = {};
1392
- const encAggregates: Record<string, { scope: string; fields: string[]; delegatable: false }> = {};
1432
+ const visAggregates: Record<string, { requires: string; ownerField?: string }> = {};
1433
+ const encAggregates: Record<string, { scope: string; fields: string[]; delegatable: false; owned?: true; ownerField?: string }> = {};
1393
1434
  // ATTRIBUTED READS (field_provenance.md): aggregate TYPE → its attributed field
1394
1435
  // names — the self-describing nomosAttributedGate era key. Omit-when-empty.
1395
1436
  const attrAggregates: Record<string, string[]> = {};
1396
1437
  for (const a of [...manifest.aggregates].sort((x, y) => (x.id < y.id ? -1 : x.id > y.id ? 1 : 0))) {
1397
- if (a.requiresRead !== undefined) visAggregates[a.id] = { requires: a.requiresRead };
1438
+ if (a.requiresRead !== undefined) {
1439
+ visAggregates[a.id] = {
1440
+ requires: a.requiresRead,
1441
+ ...(a.owned !== undefined ? { ownerField: a.owned.ownerField } : {}),
1442
+ };
1443
+ }
1398
1444
  if (a.attributedFields !== undefined && a.attributedFields.length > 0) {
1399
1445
  attrAggregates[a.id] = [...a.attributedFields];
1400
1446
  }
1401
1447
  // THE ENCRYPTION GATE (slice 2): an aggregate with encrypted field(s) — its scope
1402
1448
  // is its read cap (aggregate()/the gate guarantee they coincide). delegatable:false.
1403
1449
  if (a.encryptedFields !== undefined && a.encryptedFields.length > 0 && a.requiresRead !== undefined) {
1404
- encAggregates[a.id] = { scope: a.requiresRead, fields: [...a.encryptedFields], delegatable: false };
1450
+ encAggregates[a.id] = {
1451
+ scope: a.requiresRead,
1452
+ fields: [...a.encryptedFields],
1453
+ delegatable: false,
1454
+ ...(a.owned !== undefined ? { owned: true as const, ownerField: a.owned.ownerField } : {}),
1455
+ };
1405
1456
  }
1406
1457
  }
1407
1458
  // THE CAPABILITY DECLARATION IN LAW (Full-A — capability_marketplace.md §4): the
package/src/usd_layers.ts CHANGED
@@ -72,6 +72,7 @@ import {
72
72
  type UsdQuery,
73
73
  } from "./usd.js";
74
74
  import { composeSharedRelationTypes } from "./relation_composition.js";
75
+ import { withBusinessCorrectionLayer } from "./framework/business_corrections.js";
75
76
 
76
77
  /** The layer-file format marker (customLayerData `nomos:format`). */
77
78
  export const NOMOS_USD_LAYER_FORMAT = "nomos.usd-layer.v1";
@@ -228,6 +229,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
228
229
  ...(layer.nomosForeignRefs !== undefined
229
230
  ? [`${indent}custom string nomos:foreignRefsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosForeignRefs)))}`]
230
231
  : []),
232
+ ...(layer.nomosFlowGate !== undefined
233
+ ? [`${indent}custom string nomos:flowGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosFlowGate)))}`]
234
+ : []),
231
235
  ];
232
236
  }
233
237
 
@@ -265,10 +269,12 @@ export function emitLayeredDomain(
265
269
  if (modules.length === 0) {
266
270
  throw new Error(`usd-layers: domain '${domain}' has no modules to layer`);
267
271
  }
272
+ const normalizedModules = withBusinessCorrectionLayer(modules.map((entry) => entry.module));
268
273
  const out: EmittedModuleLayer[] = [];
269
274
  const seenPaths = new Set<string>();
270
- for (const m of modules) {
271
- const doc = emitUsd([{ path: `/Nomos/${domain}`, module: m.module }]);
275
+ for (const [index, m] of modules.entries()) {
276
+ const module = normalizedModules[index]!;
277
+ const doc = emitUsd([{ path: `/Nomos/${domain}`, module }]);
272
278
  const layer = doc.layers[0]!;
273
279
  const overPaths = new Set<string>();
274
280
  for (const prim of layer.prims) {
@@ -277,7 +283,7 @@ export function emitLayeredDomain(
277
283
  out.push({
278
284
  name: m.name,
279
285
  layer,
280
- usda: usdaLayerText(domain, m.name, layer, m.module, overPaths),
286
+ usda: usdaLayerText(domain, m.name, layer, module, overPaths),
281
287
  });
282
288
  for (const prim of layer.prims) seenPaths.add(prim.path);
283
289
  }
@@ -1031,6 +1037,43 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
1031
1037
  };
1032
1038
  }
1033
1039
  }
1040
+ // THE FLOW GATE: a stronger directive replaces the weaker declaration wholesale, so its
1041
+ // harvested endorsement flows must do the same (including replacing them with none). Other
1042
+ // flow rows union by their stable site. Both halves retain emitUsd's canonical sort order.
1043
+ let flowGate: UsdLayer["nomosFlowGate"];
1044
+ {
1045
+ type Endorse = NonNullable<NonNullable<UsdLayer["nomosFlowGate"]>["endorse"]>[number];
1046
+ type Declassify = NonNullable<NonNullable<UsdLayer["nomosFlowGate"]>["declassify"]>[number];
1047
+ const endorseByDirective = new Map<string, Endorse[]>();
1048
+ const declassifyBySite = new Map<string, Declassify>();
1049
+ for (const layer of stack) {
1050
+ const touchedDirectives = new Set(
1051
+ layer.nomosIdentityManifest?.directives.map((directive) => directive.id) ?? [],
1052
+ );
1053
+ for (const directiveId of touchedDirectives) endorseByDirective.delete(directiveId);
1054
+ for (const row of layer.nomosFlowGate?.endorse ?? []) {
1055
+ const rows = endorseByDirective.get(row.directiveId) ?? [];
1056
+ rows.push(row);
1057
+ endorseByDirective.set(row.directiveId, rows);
1058
+ }
1059
+ for (const row of layer.nomosFlowGate?.declassify ?? []) declassifyBySite.set(row.site, row);
1060
+ }
1061
+ const declassify = [...declassifyBySite.values()].sort((a, b) =>
1062
+ a.site < b.site ? -1 : a.site > b.site ? 1 : 0,
1063
+ );
1064
+ const endorse = [...endorseByDirective.values()].flat().sort((a, b) =>
1065
+ a.directiveId !== b.directiveId ? (a.directiveId < b.directiveId ? -1 : 1)
1066
+ : a.sink !== b.sink ? (a.sink < b.sink ? -1 : 1)
1067
+ : a.facet < b.facet ? -1 : a.facet > b.facet ? 1 : 0,
1068
+ );
1069
+ if (declassify.length > 0 || endorse.length > 0) {
1070
+ flowGate = {
1071
+ v: 1,
1072
+ ...(declassify.length > 0 ? { declassify } : {}),
1073
+ ...(endorse.length > 0 ? { endorse } : {}),
1074
+ };
1075
+ }
1076
+ }
1034
1077
  return {
1035
1078
  path,
1036
1079
  prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
@@ -1060,6 +1103,7 @@ return {
1060
1103
  ...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
1061
1104
  ...(foreignRefs !== undefined ? { nomosForeignRefs: foreignRefs } : {}),
1062
1105
  ...(contracts !== undefined ? { nomosContracts: contracts } : {}),
1106
+ ...(flowGate !== undefined ? { nomosFlowGate: flowGate } : {}),
1063
1107
  };
1064
1108
  });
1065
1109
 
@@ -1203,6 +1247,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
1203
1247
  const valueObjectsByDomain = new Map<string, UsdLayer["nomosValueObjects"]>();
1204
1248
  const foreignRefsByDomain = new Map<string, UsdLayer["nomosForeignRefs"]>();
1205
1249
  const contractsByDomain = new Map<string, UsdLayer["nomosContracts"]>();
1250
+ const flowGateByDomain = new Map<string, UsdLayer["nomosFlowGate"]>();
1206
1251
  for (const p of prims) {
1207
1252
  if (p.attrs.get("nomos:kind") === "domain") {
1208
1253
  const domain = attrString(p, "nomos:domain");
@@ -1317,6 +1362,10 @@ export function parseUsdaDocument(text: string): UsdDocument {
1317
1362
  if (typeof ctHex === "string") {
1318
1363
  contractsByDomain.set(domain, JSON.parse(unhexUtf8(ctHex)) as UsdLayer["nomosContracts"]);
1319
1364
  }
1365
+ const flowGateHex = p.attrs.get("nomos:flowGateHex");
1366
+ if (typeof flowGateHex === "string") {
1367
+ flowGateByDomain.set(domain, JSON.parse(unhexUtf8(flowGateHex)) as UsdLayer["nomosFlowGate"]);
1368
+ }
1320
1369
  }
1321
1370
  }
1322
1371
 
@@ -1431,6 +1480,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
1431
1480
  const valueObjects = valueObjectsByDomain.get(domain);
1432
1481
  const foreignRefs2 = foreignRefsByDomain.get(domain);
1433
1482
  const contracts2 = contractsByDomain.get(domain);
1483
+ const flowGate2 = flowGateByDomain.get(domain);
1434
1484
  return {
1435
1485
  path: `/Nomos/${domain}`,
1436
1486
  prims: [...b.prims].sort(byPath),
@@ -1460,6 +1510,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
1460
1510
  ...(valueObjects !== undefined ? { nomosValueObjects: valueObjects } : {}),
1461
1511
  ...(foreignRefs2 !== undefined ? { nomosForeignRefs: foreignRefs2 } : {}),
1462
1512
  ...(contracts2 !== undefined ? { nomosContracts: contracts2 } : {}),
1513
+ ...(flowGate2 !== undefined ? { nomosFlowGate: flowGate2 } : {}),
1463
1514
  };
1464
1515
  })
1465
1516
  .sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
@@ -39,9 +39,10 @@
39
39
  * value. When subfields must merge independently, model them as aggregate fields or a
40
40
  * `t.map(...)` — that is what aggregates/maps are for.
41
41
  *
42
- * ERA SAFETY: the VO metadata never enters the kernel wire schema or the canonical
43
- * manifest — a domain's domainHash with a VO field equals the same domain with a plain
44
- * `t.jsonObject()` field. The `nomosValueObjects` USD key is emitted IFF a domain's
42
+ * SCHEMA IDENTITY: the VO metadata never changes the driver-only kernel wire schema, but
43
+ * its canonical descriptor is included in the stable field value contract. A VO shape
44
+ * change therefore changes field/law identity so a gradual rollout cannot cast an old
45
+ * observation as the new type. The `nomosValueObjects` USD key is emitted IFF a domain's
45
46
  * aggregates carry ≥1 VO field; a VO-free domain's package bytes are untouched.
46
47
  *
47
48
  * THE CONSTRAINED FIELD SET (no fallback — anything else throws at compile):
@@ -228,9 +229,10 @@ function zodNumberIsInt(zt: z.ZodTypeAny): boolean {
228
229
  /**
229
230
  * THE UNIT BRAND (dart_codegen_parity.md P2) — `unit("Millimetres", z.number())` stamps the
230
231
  * schema so the descriptor carries the unit name and the generated Dart wraps the field in a
231
- * ZERO-COST extension type (`Millimetres`). Compile-time only: the wire value stays a plain
232
- * JSON number; validation is untouched; domainHash-inert. v1 = opaque tags — NO generated
233
- * arithmetic or conversions (a physics library is not a type system).
232
+ * ZERO-COST extension type (`Millimetres`). The wire value stays a plain JSON number and
233
+ * validation is untouched. When the unit appears in an aggregate value-object descriptor it
234
+ * is part of that field's stable value contract; payload-only use remains codegen metadata.
235
+ * v1 = opaque tags — NO generated arithmetic or conversions (a physics library is not a type system).
234
236
  */
235
237
  export const NOMOS_UNIT_BRAND: unique symbol = Symbol.for("nomos.unit");
236
238
 
@@ -253,7 +255,8 @@ export function unit<Z extends z.ZodTypeAny>(name: string, inner: Z): Z {
253
255
  * wire-identical to `z.string()` but the generated Dart field is the aggregate's TYPED id
254
256
  * class (`SiteId`), and the generated TS references the branded alias. Passing another
255
257
  * aggregate's id where a SiteId belongs becomes a COMPILE error. Payload-side only — the
256
- * kernel keeps seeing a string; validation/domainHash untouched.
258
+ * kernel keeps seeing a string; validation and the aggregate field contract are untouched
259
+ * because this is a directive-payload annotation.
257
260
  */
258
261
  export const NOMOS_IDOF_BRAND: unique symbol = Symbol.for("nomos.idOf");
259
262
 
@@ -270,8 +273,7 @@ export function idOf(aggregate: { id: string } | string): z.ZodType<string> {
270
273
 
271
274
  /**
272
275
  * THE BRANDED-ID BRAND (dart_codegen_parity.md P2 — the standalone twin of {@link idOf}) —
273
- * `brandedId("SiteId")` is a NOMINAL string: wire-identical to `z.string()` (validation +
274
- * domainHash untouched) but the generated TS is a branded alias
276
+ * `brandedId("SiteId")` is a NOMINAL string: wire-identical to `z.string()` but the generated TS is a branded alias
275
277
  * (`type SiteId = string & { readonly __brand }`) and the generated Dart a zero-cost
276
278
  * extension type (`extension type const SiteId(String value) {}`), so passing a bare string
277
279
  * — or another brand — where a `SiteId` belongs is a COMPILE error. Unlike `idOf`, it is NOT
@@ -430,9 +432,9 @@ const VO_NAME_RE = /^[A-Za-z][A-Za-z0-9_]*$/;
430
432
 
431
433
  /**
432
434
  * THE VO BRAND (slice A of vo_authoring_end_to_end.md) — `valueObject()` stamps its zod schema
433
- * with the VO's {@link ValueObjectFieldMeta} under this symbol, COMPILE-TIME ONLY: the brand is
434
- * a non-enumerable property, never serialized, never read by the engine's validation path, so a
435
- * branded schema's package bytes (and domainHash) are byte-identical to an unbranded one.
435
+ * with the VO's {@link ValueObjectFieldMeta} under this symbol. The property itself is
436
+ * non-enumerable, never serialized and never read by the engine's validation path. When attached
437
+ * to an aggregate field, its canonical descriptor deliberately enters that field's stable value contract.
436
438
  * Every generated-surface introspector (Dart/TS payload gen, interface kinds, proof synthesis)
437
439
  * consults {@link voMetaOf} BEFORE its raw zod-kind switch — the VO keeps its NAME on every
438
440
  * surface instead of degrading to an anonymous json object.
@@ -565,8 +567,9 @@ function makeValueObject<T>(
565
567
  ...(geometry !== undefined ? { geometry } : {}),
566
568
  ...(example !== undefined ? { example } : {}),
567
569
  };
568
- // THE BRAND (slice A): compile-time identity on the schema itself — non-enumerable, never
569
- // serialized, domainHash-inert. voMetaOf() is the one resolver every introspector uses.
570
+ // THE BRAND (slice A): compile-time identity on the schema itself — non-enumerable and never
571
+ // serialized directly. voMetaOf() is the one resolver every introspector uses; aggregate
572
+ // fields separately copy the canonical descriptor into their stable value contract.
570
573
  Object.defineProperty(zodSchema, NOMOS_VO_BRAND, { value: meta, enumerable: false });
571
574
  const vo: ValueObject<T> = {
572
575
  __isValueObject: true,
@@ -583,10 +586,10 @@ function makeValueObject<T>(
583
586
  },
584
587
  merge(driver: Driver): Field<T, "json"> {
585
588
  const k = driver.kind;
586
- if (k !== "Lww" && k !== "LwwCausal" && k !== "LwwBy") {
589
+ if (k !== "FirstWriterWins" && k !== "Lww" && k !== "LwwCausal" && k !== "LwwBy") {
587
590
  throw new Error(
588
591
  `valueObject '${name}': .merge(${k}) is not a valid value-object field merge — a VO is ONE ` +
589
- `Lww-class json leaf replaced WHOLESALE, so only Lww / LwwCausal / LwwBy apply. Field-level ` +
592
+ `wholesale json leaf, so only FirstWriterWins / Lww / LwwCausal / LwwBy apply. Field-level ` +
590
593
  `merge composition of a VO's subfields is a later slice; model independently-merging subfields ` +
591
594
  `as aggregate fields or a t.map(...).`,
592
595
  );
@@ -597,7 +600,7 @@ function makeValueObject<T>(
597
600
  // THE HANDLE IS A FIRST-CLASS ZOD SCHEMA (vo_first_class.md slice 1): delegate zod's
598
601
  // internals so the raw HANDLE composes DIRECTLY where a zod is expected — `z.array(Vo)`,
599
602
  // `z.object({ f: Vo })`, a directive payload — with NO `.zod` peeling. Non-enumerable (never
600
- // serialized, domainHash-inert) and stamped with the VO brand so describeZod / voMetaOf resolve
603
+ // serialized directly) and stamped with the VO brand so describeZod / voMetaOf resolve
601
604
  // the handle exactly like its `.zod`.
602
605
  const inner = zodSchema as unknown as { _zod?: unknown; _def?: unknown; def?: unknown };
603
606
  Object.defineProperty(vo, "_zod", { get: () => inner._zod, enumerable: false });
@@ -918,8 +921,9 @@ export function collectValueObjectDescriptors(
918
921
  * ONE VO↔FIELD ATTACHMENT (the IR pointer): aggregate `aggregate`'s field `field`
919
922
  * stores value object `vo`. Rides the additive `nomosValueObjects` USD key as
920
923
  * `usages` so tooling (the scene projector, the explorer glyphs) can attach a VO
921
- * molecule to the exact field that carries it. NEVER in the canonical
922
- * manifest/domainHash — a VO field's wire schema stays a plain `jsonObject` leaf.
924
+ * molecule to the exact field that carries it. The pointer itself is not in the canonical
925
+ * manifest; the VO descriptor is already hash-bearing through the stable field value contract,
926
+ * while the kernel wire schema stays a plain `jsonObject` leaf.
923
927
  */
924
928
  export interface ValueObjectUsage {
925
929
  readonly aggregate: string;
package/src/wire.ts CHANGED
@@ -211,6 +211,7 @@ export interface WireReadAttestation {
211
211
  * DSL driver encodes to yet — added here only as the DSL gains a constant for them.)
212
212
  */
213
213
  export type WireDriver =
214
+ | "FirstWriterWins"
214
215
  | "Lww"
215
216
  | "LwwCausal"
216
217
  | "AddWins"