@githolon/dsl 0.107.4 → 0.108.1

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 (93) hide show
  1. package/dart/CHANGELOG.md +24 -0
  2. package/dart/lib/src/application_framework_calls.dart +299 -0
  3. package/dart/lib/src/generated_call.dart +96 -0
  4. package/dart/lib/src/nms_generated_runtime.dart +29 -0
  5. package/dart/lib/src/node_runner_transport.dart +77 -1
  6. package/dart/lib/src/result_usda.dart +487 -0
  7. package/dart/lib/src/runner/node-entry.mjs +10634 -6122
  8. package/dart/lib/src/runner/offline-install.mjs +4187 -2716
  9. package/dart/lib/src/runtime_v2_gateway.dart +54 -0
  10. package/dart/lib/src/runtime_v2_transport.dart +117 -0
  11. package/dart/lib/src/session_bound.dart +7 -2
  12. package/dart/lib/src/test_harness.dart +373 -282
  13. package/dart/lib/src/webview_bridge.dart +2146 -725
  14. package/dart/pubspec.lock +17 -1
  15. package/dart/pubspec.yaml +3 -2
  16. package/dart/test/acquire_session_credentials_test.dart +1 -1
  17. package/dart/test/acquire_session_test.dart +68 -14
  18. package/dart/test/adv_realm_interference_test.dart +6 -4
  19. package/dart/test/cluster_births_test.dart +11 -8
  20. package/dart/test/cluster_local_autosync_test.dart +1 -1
  21. package/dart/test/cluster_scale_test.dart +3 -2
  22. package/dart/test/dlq_session_surface_test.dart +52 -35
  23. package/dart/test/first_app_gauntlet_test.dart +1 -1
  24. package/dart/test/local_sync_inert_test.dart +23 -26
  25. package/dart/test/offer_authentication_proof_test.dart +193 -0
  26. package/dart/test/result_usda_test.dart +49 -0
  27. package/dart/test/session_bound_test.dart +8 -11
  28. package/dart/test/stale_bundle_test.dart +18 -3
  29. package/dart/test/test_harness_test.dart +14 -27
  30. package/dart/test/webview_bridge_test.dart +504 -83
  31. package/dart_types/CHANGELOG.md +17 -0
  32. package/dart_types/lib/nomos_kernel_protocol.dart +11 -0
  33. package/dart_types/lib/src/custody.dart +64 -19
  34. package/dart_types/lib/src/local_work_rejected.dart +30 -45
  35. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pb.dart +169 -0
  36. package/dart_types/lib/src/proto/nomos/custody/v1/snapshot.pbenum.dart +11 -0
  37. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +17945 -0
  38. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +933 -0
  39. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pb.dart +245 -0
  40. package/dart_types/lib/src/proto/nomos/kernel/v1/progress.pbenum.dart +160 -0
  41. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +8664 -0
  42. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pbenum.dart +369 -0
  43. package/dart_types/lib/src/surfaced.dart +30 -25
  44. package/dart_types/pubspec.lock +16 -0
  45. package/dart_types/pubspec.yaml +3 -1
  46. package/dart_types/test/surfaced_test.dart +2 -1
  47. package/package.json +1 -1
  48. package/runtime/generated-application-host.js +5760 -1879
  49. package/src/authoring.ts +3 -2
  50. package/src/birth_meter_ensure.ts +2 -0
  51. package/src/births.ts +21 -0
  52. package/src/build_package.ts +30 -0
  53. package/src/business_birth.ts +41 -35
  54. package/src/codegen_call_usda.ts +1111 -0
  55. package/src/codegen_dart.ts +207 -282
  56. package/src/codegen_dart_application.ts +195 -252
  57. package/src/codegen_dart_client.ts +1978 -593
  58. package/src/codegen_dart_framework_calls.ts +291 -0
  59. package/src/codegen_dart_scenario.ts +7 -0
  60. package/src/codegen_proof.ts +3 -4
  61. package/src/codegen_provider_dart.ts +21 -7
  62. package/src/codegen_query_usda.ts +214 -0
  63. package/src/codegen_result_usda.ts +685 -0
  64. package/src/codegen_ts.ts +3945 -1111
  65. package/src/compile_package_main.ts +392 -46
  66. package/src/directive.ts +19 -4
  67. package/src/engine_entry.ts +45 -4
  68. package/src/framework/application_home.ts +4 -0
  69. package/src/framework/auth_provider_law.ts +296 -20
  70. package/src/framework/birth_certificate_law.ts +24 -0
  71. package/src/framework/home.ts +83 -40
  72. package/src/framework/owned_custody.ts +25 -12
  73. package/src/framework/owned_custody_value.ts +51 -0
  74. package/src/framework/relations.ts +47 -21
  75. package/src/framework/signing_identity.ts +100 -17
  76. package/src/framework/workspaces.ts +39 -10
  77. package/src/framework_call_definitions.ts +53 -0
  78. package/src/framework_read_definitions.ts +45 -0
  79. package/src/identity_frontier.ts +58 -0
  80. package/src/index.ts +22 -1
  81. package/src/intent.ts +216 -16
  82. package/src/law_check.ts +19 -1
  83. package/src/law_graph.ts +57 -8
  84. package/src/law_graph_usda.ts +73 -5
  85. package/src/law_value_type.ts +106 -19
  86. package/src/read.ts +47 -0
  87. package/src/read_projection_namespace.ts +7 -0
  88. package/src/shape_compat.ts +44 -42
  89. package/src/stable_ids.ts +37 -2
  90. package/src/test_call_usda.ts +83 -0
  91. package/src/wire.ts +36 -2
  92. package/src/wire_encode.ts +45 -1
  93. package/src/workspace_sharing.ts +27 -10
@@ -1,5 +1,16 @@
1
1
  import type { z } from "zod";
2
2
  import { enumTypeNameOf } from "./enum_type.js";
3
+ import { isGitObjectIdSchema, isSha256DigestSchema } from "./fields.js";
4
+ import { recipientMetaOf } from "./recipient.js";
5
+ import {
6
+ NOMOS_UNIT_BRAND,
7
+ brandedIdMetaOf,
8
+ idOfMetaOf,
9
+ idOfTargetKindOf,
10
+ refOfMetaOf,
11
+ versionedRefOfMetaOf,
12
+ voMetaOf,
13
+ } from "./value_object.js";
3
14
  import {
4
15
  zodArrayElement,
5
16
  zodDef,
@@ -24,7 +35,33 @@ export interface LawValueMember {
24
35
  readonly value: LawValueType;
25
36
  }
26
37
 
27
- export type LawValueType =
38
+ /**
39
+ * Nominal meaning carried by an otherwise structural value.
40
+ *
41
+ * These markers are lowered from the authored DSL once, into the transient law
42
+ * graph. Language generators must consume these markers instead of consulting
43
+ * Zod brands independently. They are semantic type facts, not host API or
44
+ * presentation metadata.
45
+ */
46
+ export type LawValueSemantic =
47
+ | { readonly kind: "sha256-digest" | "git-object-id" }
48
+ | { readonly kind: "branded-id" | "unit" | "value-object"; readonly name: string }
49
+ | { readonly kind: "recipient"; readonly subject: "User" }
50
+ | {
51
+ readonly kind: "id-reference" | "address-reference";
52
+ readonly target: string;
53
+ readonly targetKind: "aggregate" | "contract";
54
+ }
55
+ | {
56
+ readonly kind: "versioned-reference";
57
+ readonly target: string;
58
+ readonly targetKind: "aggregate" | "contract";
59
+ readonly series: string;
60
+ readonly seriesKind: "aggregate" | "contract";
61
+ readonly currentField: string;
62
+ };
63
+
64
+ type LawValueShape =
28
65
  | { readonly kind: "string" | "number" | "integer"; readonly constraints: readonly LawValueConstraint[] }
29
66
  | { readonly kind: "boolean" | "null" | "unknown" }
30
67
  | { readonly kind: "enum"; readonly values: readonly string[]; readonly name?: string }
@@ -38,6 +75,8 @@ export type LawValueType =
38
75
  | { readonly kind: "recursive"; readonly anchor: string; readonly value: LawValueType }
39
76
  | { readonly kind: "recursive-ref"; readonly anchor: string };
40
77
 
78
+ export type LawValueType = LawValueShape & { readonly semantic?: LawValueSemantic };
79
+
41
80
  interface ZodCheck {
42
81
  readonly check?: string;
43
82
  readonly minimum?: number;
@@ -77,65 +116,72 @@ function lower(
77
116
  ): LawValueType {
78
117
  const kind = zodKind(schema);
79
118
  const def = zodDef(schema) as ZodExtendedDef;
119
+ // Omission/null wrappers are structural. Their contained value owns the
120
+ // nominal marker, so generators can unwrap them without duplicating meaning.
121
+ const semantic = kind === "optional" || kind === "default" || kind === "nullable"
122
+ ? undefined
123
+ : semanticOf(schema);
124
+ const finish = (value: LawValueShape): LawValueType =>
125
+ semantic === undefined ? value : { ...value, semantic };
80
126
  switch (kind) {
81
127
  case "string":
82
- return { kind: "string", constraints: constraintsOf(def) };
128
+ return finish({ kind: "string", constraints: constraintsOf(def) });
83
129
  case "number": {
84
130
  const constraints = constraintsOf(def);
85
131
  const integer = def.checks?.some((check) => checkDef(check).format === "safeint") === true;
86
- return { kind: integer ? "integer" : "number", constraints };
132
+ return finish({ kind: integer ? "integer" : "number", constraints });
87
133
  }
88
134
  case "boolean":
89
- return { kind: "boolean" };
135
+ return finish({ kind: "boolean" });
90
136
  case "null":
91
- return { kind: "null" };
137
+ return finish({ kind: "null" });
92
138
  case "unknown":
93
139
  case "any":
94
- return { kind: "unknown" };
140
+ return finish({ kind: "unknown" });
95
141
  case "enum": {
96
142
  const name = enumTypeNameOf(schema);
97
- return {
143
+ return finish({
98
144
  kind: "enum",
99
145
  values: zodEnumValues(schema).sort(compareText),
100
146
  ...(name === undefined ? {} : { name }),
101
- };
147
+ });
102
148
  }
103
149
  case "literal":
104
- return { kind: "literal", value: literalOf(def, path) };
150
+ return finish({ kind: "literal", value: literalOf(def, path) });
105
151
  case "array":
106
- return { kind: "list", element: lower(zodArrayElement(schema), `${path}[]`, activeLazy) };
152
+ return finish({ kind: "list", element: lower(zodArrayElement(schema), `${path}[]`, activeLazy) });
107
153
  case "record": {
108
154
  if (def.valueType === undefined) fail(path, "record has no value type");
109
- return { kind: "map", value: lower(def.valueType, `${path}.*`, activeLazy) };
155
+ return finish({ kind: "map", value: lower(def.valueType, `${path}.*`, activeLazy) });
110
156
  }
111
157
  case "object":
112
- return {
158
+ return finish({
113
159
  kind: "record",
114
160
  fields: Object.entries(zodObjectShape(schema))
115
161
  .sort(([a], [b]) => compareText(a, b))
116
162
  .map(([name, field]) => lowerMember(name, field, path, activeLazy)),
117
- };
163
+ });
118
164
  case "union":
119
- return {
165
+ return finish({
120
166
  kind: "sum",
121
167
  options: (def.options ?? []).map((option, index) =>
122
168
  lower(option, `${path}|${index}`, activeLazy)),
123
- };
169
+ });
124
170
  case "optional": {
125
171
  if (def.innerType === undefined) fail(path, "optional has no inner type");
126
172
  return lower(def.innerType, path, activeLazy);
127
173
  }
128
174
  case "nullable": {
129
175
  if (def.innerType === undefined) fail(path, "nullable has no inner type");
130
- return { kind: "nullable", value: lower(def.innerType, path, activeLazy) };
176
+ return finish({ kind: "nullable", value: lower(def.innerType, path, activeLazy) });
131
177
  }
132
178
  case "default": {
133
179
  if (def.innerType === undefined) fail(path, "default has no inner type");
134
- return {
180
+ return finish({
135
181
  kind: "default",
136
182
  value: lower(def.innerType, path, activeLazy),
137
183
  defaultValue: constant(def.defaultValue, path, "default"),
138
- };
184
+ });
139
185
  }
140
186
  case "lazy": {
141
187
  const existing = activeLazy.get(schema);
@@ -144,13 +190,54 @@ function lower(
144
190
  activeLazy.set(schema, path);
145
191
  const value = lower(def.getter(), path, activeLazy);
146
192
  activeLazy.delete(schema);
147
- return { kind: "recursive", anchor: path, value };
193
+ return finish({ kind: "recursive", anchor: path, value });
148
194
  }
149
195
  default:
150
196
  fail(path, `unsupported Zod type '${kind}'`);
151
197
  }
152
198
  }
153
199
 
200
+ function semanticOf(schema: z.ZodTypeAny): LawValueSemantic | undefined {
201
+ if (isSha256DigestSchema(schema)) return { kind: "sha256-digest" };
202
+ if (isGitObjectIdSchema(schema)) return { kind: "git-object-id" };
203
+ const addressed = refOfMetaOf(schema);
204
+ if (addressed !== undefined) {
205
+ return {
206
+ kind: "address-reference",
207
+ target: addressed.aggregateType,
208
+ targetKind: addressed.targetKind,
209
+ };
210
+ }
211
+ const versioned = versionedRefOfMetaOf(schema);
212
+ if (versioned !== undefined) {
213
+ return {
214
+ kind: "versioned-reference",
215
+ target: versioned.targetAggregateType,
216
+ targetKind: versioned.targetKind,
217
+ series: versioned.seriesAggregateType,
218
+ seriesKind: versioned.seriesKind,
219
+ currentField: versioned.currentField,
220
+ };
221
+ }
222
+ const recipient = recipientMetaOf(schema);
223
+ if (recipient !== undefined) return { kind: "recipient", subject: recipient };
224
+ const target = idOfMetaOf(schema);
225
+ if (target !== undefined) {
226
+ return {
227
+ kind: "id-reference",
228
+ target,
229
+ targetKind: idOfTargetKindOf(schema) ?? "aggregate",
230
+ };
231
+ }
232
+ const brand = brandedIdMetaOf(schema);
233
+ if (brand !== undefined) return { kind: "branded-id", name: brand };
234
+ const unit = (schema as unknown as Record<symbol, unknown>)[NOMOS_UNIT_BRAND];
235
+ if (typeof unit === "string") return { kind: "unit", name: unit };
236
+ const valueObject = voMetaOf(schema);
237
+ if (valueObject !== undefined) return { kind: "value-object", name: valueObject.name };
238
+ return undefined;
239
+ }
240
+
154
241
  function lowerMember(
155
242
  name: string,
156
243
  schema: z.ZodTypeAny,
package/src/read.ts CHANGED
@@ -17,6 +17,9 @@
17
17
  * it read to justify them. The dev never sees `nomos.read` — they call typed DSL queries; the library
18
18
  * routes them here. (See `docs/aggregate_lifecycle_and_relations.md`.)
19
19
  */
20
+ import { z } from "zod";
21
+ import type { AggregateHandle, ReadModelRow, StoredFields } from "./aggregate.js";
22
+ import type { Field } from "./fields.js";
20
23
  import type { QueryDecl } from "./query.js";
21
24
  import type { WireRead, WireReadAttestation } from "./wire.js";
22
25
  import { NomosSourceFrontier, sourceFrontierId, __trustFor, __isMonotoneForeignQuery, __ensure } from "./attested_core.js";
@@ -103,6 +106,50 @@ export function readFrameworkDeclared<T = unknown>(queryId: string, args: Record
103
106
  return result;
104
107
  }
105
108
 
109
+ type ProjectionShape<F extends Record<string, Field>> = {
110
+ [K in keyof F]: z.ZodOptional<F[K]["zod"]>;
111
+ };
112
+
113
+ /**
114
+ * Decode one framework query's standard projected rows from the SAME aggregate field declarations
115
+ * that authored its law. The only type assertion is at the TypeScript `Object.fromEntries` seam:
116
+ * every runtime member is still obtained from `Field.zod`, so there is no second field-name/type list
117
+ * and no unvalidated object crosses into a framework plan.
118
+ *
119
+ * Historical captured reads may contain either the current `{ type, id, data }` projection envelope
120
+ * or the earlier flat data row. Both pass through the identical derived schema before being returned.
121
+ */
122
+ export function readFrameworkDeclaredRows<
123
+ const Id extends string,
124
+ F extends Record<string, Field>,
125
+ >(
126
+ query: QueryDecl,
127
+ aggregate: AggregateHandle<Id, F>,
128
+ args: Record<string, string>,
129
+ ): Array<ReadModelRow<F>> {
130
+ if (query.returns !== aggregate.id) {
131
+ throw new Error(
132
+ `framework read('${query.id}') declares '${query.returns}', not '${aggregate.id}'`,
133
+ );
134
+ }
135
+ const fields: Readonly<Record<string, Field>> = aggregate.fields;
136
+ const shape = Object.fromEntries(
137
+ Object.entries(fields).map(([name, field]) => [name, field.zod.optional()]),
138
+ ) as ProjectionShape<StoredFields<F>>;
139
+ const data = z.object(shape).strict() as z.ZodType<ReadModelRow<F>>;
140
+ // Recognize the envelope before validating its data. If the payload is malformed, validation must
141
+ // report the actual aggregate field (for example a historical `keys` member), not fall through and
142
+ // misdiagnose the envelope's `type`/`id` metadata as flat-row fields.
143
+ const envelope = z.object({ data: z.unknown() }).passthrough();
144
+ const row = z.union([envelope, z.unknown()])
145
+ .transform((value) => {
146
+ const wrapped = envelope.safeParse(value);
147
+ return wrapped.success ? wrapped.data.data : value;
148
+ })
149
+ .pipe(data);
150
+ return z.array(row).parse(readFrameworkDeclared(query.id, args));
151
+ }
152
+
106
153
  function readForeign<T = unknown>(query: QueryDecl | { id: string }, args: Record<string, string>, from: string): T {
107
154
  // ── THE FOREIGN (attested cross-workspace) LANE (slice B — attested_reads_design.md §4.1).
108
155
  // The port answers the `{rows, attestation}` wrapper: at AUTHOR the kernel verified the
@@ -108,6 +108,13 @@ export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set
108
108
  "PlatformGrant",
109
109
  "CapabilityBinding",
110
110
  "AuthProvider",
111
+ // The kernel reconstructs the active verifier by the framework-owned aggregate
112
+ // ids/prefixes, and child law copies the same typed rows across domain boundaries.
113
+ // Namespacing these two types made identical framework queries domain-local:
114
+ // bootstrap wrote application-home.AuthProviderKey while a tenant Home birth
115
+ // read home.AuthProviderKey, silently dropping every verifier key.
116
+ "AuthProviderKey",
117
+ "AuthProviderCertificate",
111
118
  "HostnameClaim",
112
119
  // governance postures (managed / sovereign) — the tenant-owned posture singleton + consent facts
113
120
  "GovernancePosture",
@@ -23,7 +23,8 @@
23
23
  // is reported precisely).
24
24
 
25
25
  import { z } from "zod";
26
- import { introspectPayload, type DomainModule, type PayloadFieldSpec } from "./codegen_dart.js";
26
+ import type { DomainModule } from "./codegen_dart.js";
27
+ import { lowerLawValueType, type LawValueType } from "./law_value_type.js";
27
28
  import type { StableIds } from "./stable_ids_types.js";
28
29
  import type { Field } from "./fields.js";
29
30
  import { isBusinessIntent } from "./intent.js";
@@ -113,46 +114,40 @@ export interface CompatVerdict {
113
114
  readonly incompatible: string[];
114
115
  }
115
116
 
116
- /** Normalize a directive payload field to a language-neutral kind from its introspected
117
- * spec (the SAME `introspectPayload` the Dart/TS clients are generated from, so both
118
- * sides of a comparison speak one vocabulary). */
119
- function payloadFieldKind(spec: PayloadFieldSpec): string {
120
- switch (spec.toJson) {
117
+ /** Normalize the canonical language-neutral DSL value graph to the compatibility
118
+ * vocabulary. Language generators are projections of this graph, never authorities
119
+ * for it. */
120
+ function payloadFieldKind(value: LawValueType): string {
121
+ switch (value.kind) {
121
122
  case "enum":
122
123
  return "enum";
123
- case "strList":
124
- return "set";
125
- case "json":
126
- return "json";
127
- case "wrapped":
128
- // typed-id (string wire) or unit (number wire) — the interface sees the WIRE kind.
129
- return spec.wrappedScalarKind === "string" ? "string" : "double";
130
- case "ref":
131
- // NomosRef<T> is aggregate-typed in the client and remains a string id on the wire.
132
- return "string";
133
- case "addressRef":
134
- // A workspace-qualified NomosRef<T> preserves its complete address object.
135
- return "json";
136
- case "recipient":
137
- // Recipient<User> is nominal in clients and an opaque delivery address on the law wire.
124
+ case "string":
138
125
  return "string";
139
- case "vo":
140
- case "voList":
141
- // Interface kinds stay at the base lane for now (the remaining parity work in dart_codegen_parity.md
142
- // upgrades these to a first-class "vo" kind) — a VO IS a json leaf structurally.
126
+ case "integer":
127
+ return "int";
128
+ case "number":
129
+ return "double";
130
+ case "boolean":
131
+ return "bool";
132
+ case "literal":
133
+ return typeof value.value === "boolean"
134
+ ? "bool"
135
+ : typeof value.value === "number"
136
+ ? Number.isInteger(value.value) ? "int" : "double"
137
+ : typeof value.value === "string" ? "string" : "json";
138
+ case "list":
139
+ return value.element.kind === "string" ? "set" : "json";
140
+ case "nullable":
141
+ case "default":
142
+ case "recursive":
143
+ return payloadFieldKind(value.value);
144
+ case "map":
145
+ case "record":
146
+ case "sum":
147
+ case "null":
148
+ case "unknown":
149
+ case "recursive-ref":
143
150
  return "json";
144
- case "scalar":
145
- default:
146
- switch (spec.dartType) {
147
- case "int":
148
- return "int";
149
- case "double":
150
- return "double";
151
- case "bool":
152
- return "bool";
153
- default:
154
- return "string";
155
- }
156
151
  }
157
152
  }
158
153
 
@@ -198,8 +193,12 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
198
193
  const fieldEntries: Array<[string, PayloadFieldShape]> = [];
199
194
  const schema = d.businessBirth?.publicPayloadSchema ?? d.payloadSchema;
200
195
  if (schema && typeof schema === "object") {
201
- for (const spec of introspectPayload(schema as z.ZodTypeAny, enumTypes)) {
202
- fieldEntries.push([spec.name, { kind: payloadFieldKind(spec), optional: !!spec.optional }]);
196
+ const payload = lowerLawValueType(schema as z.ZodTypeAny);
197
+ if (payload.kind !== "record") {
198
+ throw new Error(`directive '${d.id}' payload must lower to a typed record`);
199
+ }
200
+ for (const field of payload.fields) {
201
+ fieldEntries.push([field.name, { kind: payloadFieldKind(field.value), optional: !field.required }]);
203
202
  }
204
203
  }
205
204
  dirEntries.push([
@@ -213,16 +212,19 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
213
212
  for (const agg of (mod.aggregates ?? []) as Array<{
214
213
  id: string;
215
214
  fields: Record<string, Field>;
216
- internalFields?: readonly string[];
217
215
  }>) {
218
216
  if (isInternal(agg)) continue;
219
217
  const label = agg.id;
220
218
  const sa = stableIds?.aggregates?.[label];
221
219
  const aggSid = sa?.sid ?? label;
222
220
  const fieldEntries: Array<[string, { kind: string; label: string }]> = [];
223
- const internal = new Set(agg.internalFields ?? []);
224
221
  for (const [fname, f] of Object.entries(agg.fields ?? {})) {
225
- if (!f || f.kind === "hasMany" || internal.has(fname)) continue;
222
+ // This is the workspace's complete structural offer and the input to law-evolution
223
+ // checking, so framework-managed stored fields belong here too. Only the generated
224
+ // client's view hides them, in publicClientInterface below. Filtering them at both
225
+ // seams makes an unchanged private field look deleted when the current offer is
226
+ // compared with the committed identity ledger.
227
+ if (!f || f.kind === "hasMany") continue;
226
228
  const fsid = sa?.fields?.[fname]?.sid ?? fname;
227
229
  const kind = sa?.fields?.[fname]?.kind ?? aggFieldReadKind(f);
228
230
  fieldEntries.push([fsid, { kind, label: fname }]);
package/src/stable_ids.ts CHANGED
@@ -92,6 +92,16 @@ export interface InferredRename {
92
92
  readonly sid: string;
93
93
  }
94
94
 
95
+ /** One authored retirement from the immediately preceding model. Retirement is
96
+ * semantic evidence that a disappearance is intentional, so it must take
97
+ * precedence over the shape heuristic used only to infer otherwise-silent
98
+ * renames. */
99
+ export interface StableIdRetirement {
100
+ readonly kind: "aggregate" | "field";
101
+ readonly aggregate: string;
102
+ readonly field?: string;
103
+ }
104
+
95
105
  /**
96
106
  * Derive the stable-id CONTINUITY for one domain from its PRIOR manifest's stableIds
97
107
  * (the chain/build holds v's sids) against the CURRENT compile's names.
@@ -120,12 +130,29 @@ export function deriveStableIdContinuity(
120
130
  /** The prior canonical manifest's aggregate schemas (keyed by PRIOR name) — when
121
131
  * supplied, tightens the field-pair rule to same-driver only. */
122
132
  priorSchemas?: Record<string, WireSchema>,
133
+ /** Explicit candidate-law retirements. Their exact prior SIDs are excluded from
134
+ * heuristic rename inference; a same-shaped replacement therefore mints its own
135
+ * identity while an undeclared, genuine rename still carries continuity. */
136
+ retirements: readonly StableIdRetirement[] = [],
123
137
  ): { continuity: StableIds; inferred: InferredRename[] } {
124
138
  const inferred: InferredRename[] = [];
125
139
  const domainSid = prior?.domain ?? mintDomainSid(domainName);
126
140
 
127
141
  const priorAggs = prior?.aggregates ?? {};
128
142
  const currentNames = new Set(currentAggregates.map((a) => a.id));
143
+ const explicitlyRetiredSids = new Set<string>();
144
+ for (const retirement of retirements) {
145
+ const priorAggregate = priorAggs[retirement.aggregate];
146
+ if (priorAggregate === undefined) continue;
147
+ if (retirement.kind === "aggregate") {
148
+ explicitlyRetiredSids.add(priorAggregate.sid);
149
+ continue;
150
+ }
151
+ const priorField = retirement.field === undefined
152
+ ? undefined
153
+ : priorAggregate.fields[retirement.field];
154
+ if (priorField !== undefined) explicitlyRetiredSids.add(priorField.sid);
155
+ }
129
156
 
130
157
  // Pass 1 — name matches.
131
158
  const resolved: Record<
@@ -151,7 +178,11 @@ export function deriveStableIdContinuity(
151
178
  // Pass 2 — the unambiguous aggregate pair (one disappeared, one appeared, same shape).
152
179
  const disappeared = Object.keys(priorAggs).filter((n) => !currentNames.has(n));
153
180
  const appeared = currentAggregates.filter((a) => resolved[a.id] === undefined);
154
- if (disappeared.length === 1 && appeared.length === 1) {
181
+ if (
182
+ disappeared.length === 1 &&
183
+ appeared.length === 1 &&
184
+ !explicitlyRetiredSids.has(priorAggs[disappeared[0]!]!.sid)
185
+ ) {
155
186
  const oldName = disappeared[0]!;
156
187
  const oldAgg = priorAggs[oldName]!;
157
188
  const newAgg = appeared[0]!;
@@ -192,7 +223,11 @@ export function deriveStableIdContinuity(
192
223
  }
193
224
  const fDisappeared = Object.keys(priorFields).filter((n) => !fieldNames.includes(n));
194
225
  const fAppeared = fieldNames.filter((n) => fields[n] === undefined);
195
- if (fDisappeared.length === 1 && fAppeared.length >= 1) {
226
+ if (
227
+ fDisappeared.length === 1 &&
228
+ fAppeared.length >= 1 &&
229
+ !explicitlyRetiredSids.has(priorFields[fDisappeared[0]!]!.sid)
230
+ ) {
196
231
  const oldF = fDisappeared[0]!;
197
232
  // Shape gate for a field rename: the DRIVER (merge law) AND the KIND (read
198
233
  // type) must be unchanged — a rename is a LABEL move. Among the appeared
@@ -0,0 +1,83 @@
1
+ import type { DomainModule } from "./codegen_dart.js";
2
+ import { identityRegistryFromModel, type CompilerIdentityModel } from "./identity_model.js";
3
+ import { discoverLawGraph } from "./law_graph.js";
4
+ import { compileSparseLawGraphUsda } from "./law_graph_usda.js";
5
+ import { stableIdsForModule } from "./stable_ids_for_module.js";
6
+ import { collectValueObjectDescriptors } from "./value_object.js";
7
+
8
+ /** Build the same canonical graph/path input the package compiler supplies to
9
+ * the generated-client encoders. Tests must not resurrect a dictionary fallback
10
+ * merely to exercise a lower-level generator directly. */
11
+ function callUsdaForTestIdentity(
12
+ module: DomainModule,
13
+ identityModel?: CompilerIdentityModel,
14
+ closureModules: readonly DomainModule[] = [module],
15
+ ) {
16
+ // A low-level generator fixture does not compose the framework birth-grant
17
+ // query that the package compiler supplies. Keep these tests honest about
18
+ // the call shape without pretending that unrelated framework reads are part
19
+ // of the tenant domain under test.
20
+ const normalize = (candidate: DomainModule): DomainModule => ({
21
+ ...candidate,
22
+ aggregates: candidate.aggregates.map((aggregate) =>
23
+ aggregate.audience === undefined
24
+ ? { ...aggregate, audience: { kind: "public" as const } }
25
+ : aggregate),
26
+ valueObjects: collectValueObjectDescriptors(candidate.aggregates),
27
+ directives: candidate.directives.map((directive) => ({
28
+ ...directive,
29
+ ...((directive as { birthsChild?: boolean }).birthsChild === true
30
+ ? { declaredQueryReads: [] }
31
+ : {}),
32
+ })),
33
+ });
34
+ const byName = new Map(
35
+ [module, ...closureModules].map((candidate) => [candidate.name, normalize(candidate)]),
36
+ );
37
+ const graphModules = [...byName.values()];
38
+ const stableIdsByName = new Map(
39
+ graphModules.map((candidate) => [candidate.name, stableIdsForModule(candidate)]),
40
+ );
41
+ const registry = identityRegistryFromModel(identityModel ?? ({
42
+ v: 1,
43
+ domains: Object.fromEntries(
44
+ graphModules.map((candidate) => [
45
+ candidate.name,
46
+ { stableIds: stableIdsByName.get(candidate.name)! },
47
+ ]),
48
+ ),
49
+ } satisfies CompilerIdentityModel));
50
+ const graphs = graphModules.map((candidate) => discoverLawGraph(
51
+ candidate,
52
+ stableIdsByName.get(candidate.name)!,
53
+ registry,
54
+ ).graph);
55
+ const graph = graphs.find((candidate) => candidate.domain.entity.label === module.name);
56
+ if (graph === undefined) throw new Error(`test law graph '${module.name}' was not composed`);
57
+ const rendered = compileSparseLawGraphUsda(
58
+ graph,
59
+ `/${module.name}.law.usda`,
60
+ graphs,
61
+ );
62
+ return { graph, paths: rendered.paths };
63
+ }
64
+
65
+ export function callUsdaForTest(module: DomainModule) {
66
+ return callUsdaForTestIdentity(module);
67
+ }
68
+
69
+ export function callUsdaForTestWithClosure(
70
+ module: DomainModule,
71
+ closureModules: readonly DomainModule[],
72
+ ) {
73
+ return callUsdaForTestIdentity(module, undefined, closureModules);
74
+ }
75
+
76
+ /** Use the compiler's committed/evolved identity ledger when a runtime regression must author
77
+ * against an already compiled fixture rather than a first-generation synthetic model. */
78
+ export function callUsdaForTestWithIdentity(
79
+ module: DomainModule,
80
+ identityModel: CompilerIdentityModel,
81
+ ) {
82
+ return callUsdaForTestIdentity(module, identityModel);
83
+ }
package/src/wire.ts CHANGED
@@ -55,9 +55,11 @@ export type WireValue =
55
55
  revokedAt?: string;
56
56
  validFromEpoch?: number;
57
57
  verifiedVia?: string;
58
- ownedCustody?: string;
58
+ ownedCustody?: WireOwnedCustody;
59
59
  };
60
60
  }
61
+ | { OwnedCustody: WireOwnedCustody }
62
+ | { OwnedDeviceRequest: WireOwnedDeviceRequest }
61
63
  | { Evidence: WireEvidenceRef }
62
64
  // First-class canonical JSON (RFC 8785 / JCS) — the committed form of a `t.json()` leaf. The
63
65
  // value rides INLINE (`{Json: <any json>}`), not as a string-of-JSON; the kernel canonicalises
@@ -69,6 +71,25 @@ export type WireValue =
69
71
  | { Position: string }
70
72
  | { TwoPhaseSet: { adds: string[]; removes: string[] } };
71
73
 
74
+ export interface WireOwnedDeviceRequest {
75
+ v: number;
76
+ workspace: string;
77
+ principal: string;
78
+ scope: string;
79
+ signingPublic: string;
80
+ wrappingPublic: string;
81
+ signature: string;
82
+ }
83
+
84
+ export interface WireOwnedCustody {
85
+ v: number;
86
+ request: WireOwnedDeviceRequest;
87
+ hpkeEpk: string;
88
+ ct: string;
89
+ keyCommitment: string;
90
+ principalWrappingPublic: string;
91
+ }
92
+
72
93
  /** `EvidenceRef { hash: ContentHash, media_type, byte_length, storage_class }` — the
73
94
  * kernel serde shape. `ContentHash(String)` serializes transparently as a bare string
74
95
  * (a newtype struct), and `StorageClass` as a bare unit-variant string. */
@@ -166,7 +187,20 @@ export interface WireIntent {
166
187
  * child's OWN gate. A verify-input WITNESS (rides the carried `captured_ports`, never the events nor the
167
188
  * OID). OMITTED when empty so a birth-free intent is byte-identical to before this existed (the era rule).
168
189
  */
169
- births?: { workspace: string; genesisChain: unknown }[];
190
+ births?: {
191
+ workspace: string;
192
+ genesisChain: unknown;
193
+ parentWorkspaceRoles?: readonly import("./births.js").WorkspaceRole[];
194
+ }[];
195
+ /** Exact target calls authored by source law. The OpenUSD bytes come from a compiler-generated
196
+ * encoder for the target Directive object; no public operation string or generic payload crosses. */
197
+ workspaceEffects?: {
198
+ destination:
199
+ | { readonly workspace: string }
200
+ | { readonly verifiedParent: true }
201
+ | { readonly verifiedSource: true };
202
+ targetCallUsda: Uint8Array;
203
+ }[];
170
204
  }
171
205
 
172
206
  /**