@githolon/dsl 0.102.4 → 0.103.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 (131) hide show
  1. package/compile_package.mjs +43 -2
  2. package/compile_proof.d.mts +47 -0
  3. package/compile_proof.mjs +343 -0
  4. package/dart/CHANGELOG.md +7 -0
  5. package/dart/lib/src/dev_cloud.dart +8 -8
  6. package/dart/lib/src/nms_generated_runtime.dart +2 -0
  7. package/dart/lib/src/runner/node-entry.mjs +5504 -1236
  8. package/dart/lib/src/runner/offline-install.mjs +4578 -528
  9. package/dart/lib/src/test_harness.dart +206 -152
  10. package/dart/lib/src/webview_bridge.dart +196 -89
  11. package/dart/pubspec.lock +1 -1
  12. package/dart/pubspec.yaml +2 -2
  13. package/dart/test/acquire_session_credentials_test.dart +4 -3
  14. package/dart/test/adv_realm_interference_test.dart +7 -6
  15. package/dart/test/cloud_bound_topology_test.dart +3 -3
  16. package/dart/test/cluster_births_test.dart +4 -3
  17. package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
  18. package/dart/test/cluster_local_autosync_test.dart +3 -2
  19. package/dart/test/cluster_scale_test.dart +3 -2
  20. package/dart/test/dlq_session_surface_test.dart +7 -6
  21. package/dart/test/first_app_gauntlet_test.dart +14 -21
  22. package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
  23. package/dart/test/four_layer_nested_topology_test.dart +4 -4
  24. package/dart/test/harness_phase_timeout_test.dart +2 -2
  25. package/dart/test/local_sync_inert_test.dart +1 -1
  26. package/dart/test/parent_signer_recovery_test.dart +2 -2
  27. package/dart/test/sealing_client_test.dart +4 -3
  28. package/dart/test/session_bound_test.dart +8 -7
  29. package/dart/test/test_harness_test.dart +13 -12
  30. package/dart/test/vo_read_typing_test.dart +4 -3
  31. package/dart/test/watch_after_offer_stall_test.dart +4 -3
  32. package/dart/test/webview_bridge_test.dart +102 -5
  33. package/dart_types/CHANGELOG.md +5 -0
  34. package/dart_types/lib/nomos_types.dart +9 -0
  35. package/dart_types/lib/src/nomos_ref.dart +20 -0
  36. package/dart_types/lib/src/optional_ref.dart +85 -0
  37. package/dart_types/lib/src/sha256_digest.dart +41 -0
  38. package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
  39. package/dart_types/pubspec.yaml +1 -1
  40. package/dart_types/test/optional_ref_test.dart +44 -0
  41. package/dart_types/test/sha256_digest_test.dart +18 -0
  42. package/package.json +9 -5
  43. package/runtime/generated-application-host.js +5365 -1105
  44. package/src/aggregate.ts +18 -0
  45. package/src/application_composition.ts +6 -32
  46. package/src/attested_core.ts +6 -3
  47. package/src/birth_meter.ts +1 -0
  48. package/src/birth_meter_ensure.ts +28 -2
  49. package/src/births.ts +1 -1
  50. package/src/build_package.ts +162 -538
  51. package/src/business_birth.ts +19 -3
  52. package/src/canonical_json.ts +21 -0
  53. package/src/check_package_main.ts +76 -35
  54. package/src/clear_directive.ts +19 -8
  55. package/src/codegen_dart.ts +305 -22
  56. package/src/codegen_dart_application.ts +844 -292
  57. package/src/codegen_dart_client.ts +1084 -407
  58. package/src/codegen_dart_scenario.ts +12 -12
  59. package/src/codegen_proof.ts +169 -39
  60. package/src/codegen_provider_dart.ts +17 -1
  61. package/src/codegen_ts.ts +298 -106
  62. package/src/compat_manifest.ts +2 -2
  63. package/src/compat_package_main.ts +8 -12
  64. package/src/compile_package_main.ts +1866 -597
  65. package/src/compose.ts +1 -1
  66. package/src/directive.ts +208 -15
  67. package/src/engine_entry.ts +165 -111
  68. package/src/evolve_preview.ts +43 -81
  69. package/src/extremum.ts +1 -1
  70. package/src/fields.ts +183 -4
  71. package/src/flow_gate.ts +75 -2
  72. package/src/framework/application_home.ts +2 -0
  73. package/src/framework/auth_provider_law.ts +2 -2
  74. package/src/framework/birth_meter.ts +7 -19
  75. package/src/framework/bootstrap.ts +0 -2
  76. package/src/framework/business_corrections.ts +9 -5
  77. package/src/framework/domain_lifecycle.ts +15 -29
  78. package/src/framework/encryption_scope.ts +2 -1
  79. package/src/framework/framework_aggregates.ts +2 -0
  80. package/src/framework/genesis.ts +16 -14
  81. package/src/framework/home.ts +20 -14
  82. package/src/framework/owned_custody.ts +9 -3
  83. package/src/framework/platform_admin.ts +3 -3
  84. package/src/framework/relations.ts +21 -13
  85. package/src/framework/signing_identity.ts +45 -40
  86. package/src/framework/workspace_invariant.ts +245 -0
  87. package/src/framework/workspaces.ts +23 -29
  88. package/src/index.ts +36 -16
  89. package/src/intent.ts +178 -6
  90. package/src/law_check.ts +32 -11
  91. package/src/law_graph.ts +2902 -0
  92. package/src/law_graph_usda.ts +1799 -0
  93. package/src/law_identity.ts +435 -0
  94. package/src/law_value_type.ts +230 -0
  95. package/src/lsp_server.ts +61 -31
  96. package/src/ops.ts +7 -0
  97. package/src/package_config.ts +141 -0
  98. package/src/package_dependencies.ts +108 -0
  99. package/src/package_dependency_usage.ts +150 -0
  100. package/src/package_source_imports.ts +71 -0
  101. package/src/plan_source_slice.ts +335 -0
  102. package/src/point_guard.ts +192 -0
  103. package/src/precondition.ts +44 -9
  104. package/src/read_projection_namespace.ts +1 -1
  105. package/src/runtime_zod.ts +44 -0
  106. package/src/secondary_ensure.ts +67 -14
  107. package/src/shape_compat.ts +1 -1
  108. package/src/stable_id_identity_bridge.ts +149 -0
  109. package/src/stable_id_lockfile.ts +278 -0
  110. package/src/stable_ids.ts +7 -2
  111. package/src/stable_ids_for_module.ts +348 -0
  112. package/src/stable_ids_types.ts +1 -1
  113. package/src/sum.ts +1 -1
  114. package/src/usd_state.ts +1 -1
  115. package/src/usda_relationship_integrity.ts +66 -0
  116. package/src/usda_text.ts +18 -0
  117. package/src/usdz.ts +78 -0
  118. package/src/value_object.ts +95 -0
  119. package/src/wire.ts +19 -0
  120. package/src/wire_encode.ts +22 -0
  121. package/src/workspace_sharding.ts +52 -36
  122. package/src/workspace_sharing.ts +28 -4
  123. package/src/zod_introspection.ts +1 -1
  124. package/dart/test/cluster_attested_test.dart +0 -201
  125. package/src/compile_engine.ts +0 -595
  126. package/src/manifest.ts +0 -1476
  127. package/src/read_projection_composition.ts +0 -117
  128. package/src/read_projection_manifest.ts +0 -115
  129. package/src/relation_composition.ts +0 -286
  130. package/src/usd.ts +0 -1967
  131. package/src/usd_layers.ts +0 -1776
@@ -7,7 +7,7 @@
7
7
  * THE EVOLVE PREVIEW — `githolon check --against <prior>` (architecture/author_time_gate.md,
8
8
  * the `--against` row of check slice 3). The evolve gate is the kernel's #58 diff-by-stable-id
9
9
  * (executor/src/law58.rs::evolve_violation): a RENAME (same sid, new label) admits silently; a
10
- * true REMOVAL / merge-driver change refuses TYPED unless the deploy JSON carries a
10
+ * true REMOVAL / merge-driver change refuses TYPED unless the installation offer carries a
11
11
  * `dispositions` acknowledgement. A changed BUSINESS VALUE CONTRACT is stricter: it needs
12
12
  * deterministic `.evolvesFrom(...)` coverage and cannot be waived by an acknowledgement.
13
13
  *
@@ -34,9 +34,10 @@ import type { Finding } from "./law_check.js";
34
34
  import type { DirectivePayloadShape, DomainInterface } from "./shape_compat.js";
35
35
  import { domainInterface } from "./shape_compat.js";
36
36
  import type { StableIds } from "./stable_ids_types.js";
37
+ import type { StableIdLockfile } from "./stable_id_lockfile.js";
38
+ export type { StableIdLockfile } from "./stable_id_lockfile.js";
37
39
  import type { DomainModule } from "./codegen_dart.js";
38
40
  import type { WireSchema } from "./wire.js";
39
- import { parseUsdaDocument } from "./usd_layers.js";
40
41
 
41
42
  // ── the normalized law shape both sides of the diff speak ──────────────────────────
42
43
 
@@ -70,7 +71,7 @@ export interface EvolveDomain {
70
71
  /** A whole package's evolve surface, keyed by DOMAIN KEY. */
71
72
  export type EvolveLaw = Record<string, EvolveDomain>;
72
73
 
73
- /** The dispositions fragment the deploy JSON carries to admit destructive evolves (the exact
74
+ /** The dispositions fragment the installDomain offer carries to admit destructive evolves (the exact
74
75
  * shape executor/src/law58.rs::dispositions_from_payload parses). */
75
76
  export interface EvolveDispositionsJson {
76
77
  retired?: string[];
@@ -92,7 +93,7 @@ function aggregatesFromStableIds(
92
93
  for (const [aggLabel, a] of Object.entries(stableIds.aggregates)) {
93
94
  const fields: EvolveAggregate["fields"] = {};
94
95
  for (const [fLabel, f] of Object.entries(a.fields ?? {})) {
95
- const driver = schemas?.[aggLabel]?.[fLabel];
96
+ const driver = f.driver ?? schemas?.[aggLabel]?.[fLabel];
96
97
  fields[f.sid] = {
97
98
  sid: f.sid,
98
99
  label: fLabel,
@@ -110,7 +111,7 @@ function aggregatesFromStableIds(
110
111
  return out;
111
112
  }
112
113
 
113
- /** Build an EvolveDomain from a DomainInterface (deploy.json's `interface`) — its aggregates are
114
+ /** Build an EvolveDomain from a diagnostic DomainInterface — its aggregates are
114
115
  * already sid-keyed with kinds+labels, its directives carry payload shapes. Drivers are absent
115
116
  * from an interface (it exposes only read kinds), so a driver-only retype is not seen from an
116
117
  * interface alone — the lockfile lane (aggregatesFromStableIds + schemas) carries them. */
@@ -130,78 +131,29 @@ function evolveDomainFromInterface(iface: DomainInterface): EvolveDomain {
130
131
  };
131
132
  }
132
133
 
133
- /** The committed `nomos.stable-ids.json` lockfile shape (compile_package_main writes it). */
134
- export interface StableIdLockfile {
135
- v: 1;
136
- domains: Record<string, { stableIds: StableIds; schemas?: Record<string, WireSchema> }>;
137
- }
138
-
139
- /** A deploy.json body (the subset this preview reads): the per-domain interface + the package
140
- * bytes (for the embedded stable-ids/driver schemas when no lockfile rides alongside). */
141
- export interface DeployJson {
142
- interface?: Record<string, DomainInterface>;
143
- packageUsda?: string;
144
- }
145
-
146
- /** Reconstruct the effective law directly from a native `.package.usda` stage. */
147
- export function usdJsonFromPackage(bundle: string): unknown | undefined {
148
- try {
149
- return parseUsdaDocument(bundle);
150
- } catch {
151
- return undefined;
152
- }
153
- }
154
-
155
- /** Per-domain-key { stableIds, schemas } parsed out of a package's embedded USD-IR (mirrors
156
- * executor/src/law58.rs::stable_ids_from_usd + the aggregate-prim schema harvest). */
157
- export function stableIdsFromUsd(
158
- usd: unknown,
159
- ): Record<string, { stableIds: StableIds; schemas: Record<string, WireSchema> }> {
160
- const out: Record<string, { stableIds: StableIds; schemas: Record<string, WireSchema> }> = {};
161
- const layers = (usd as { layers?: unknown[] })?.layers;
162
- if (!Array.isArray(layers)) return out;
163
- for (const layer of layers) {
164
- const l = layer as {
165
- path?: string;
166
- nomosStableIds?: StableIds;
167
- prims?: Array<{ kind?: string; type?: string; schema?: Record<string, unknown> }>;
168
- };
169
- const path = typeof l.path === "string" ? l.path : "";
170
- const key = path.startsWith("/Nomos/") ? path.slice("/Nomos/".length) : "";
171
- if (key === "" || l.nomosStableIds === undefined) continue;
172
- const schemas: Record<string, WireSchema> = {};
173
- for (const prim of l.prims ?? []) {
174
- if (prim.kind === "aggregate" && typeof prim.type === "string" && prim.schema) {
175
- schemas[prim.type] = prim.schema as WireSchema;
176
- }
177
- }
178
- out[key] = { stableIds: l.nomosStableIds, schemas };
179
- }
180
- return out;
181
- }
182
-
183
- /** Build an EvolveLaw from a deploy.json body: interface for directive payloads + sid-keyed read
184
- * shapes, and the embedded package USD-IR for the driver schemas (so a driver-only retype is
185
- * seen from a deploy.json alone). */
186
- export function evolveLawFromDeployJson(deploy: DeployJson): EvolveLaw {
134
+ /** Build an EvolveLaw from compiler diagnostic sidecars. The interface supplies directive and
135
+ * read shapes; the stable-id lock supplies identity, drivers and value-contract evolution. Neither
136
+ * is deployable law: canonical law remains the composed, logically named USDA closure interpreted
137
+ * by the kernel; `.law.usdz` is only its lossless one-file transport envelope. */
138
+ export function evolveLawFromInterface(
139
+ ifaces: Record<string, DomainInterface>,
140
+ lock?: StableIdLockfile,
141
+ ): EvolveLaw {
187
142
  const law: EvolveLaw = {};
188
- const ifaces = deploy.interface ?? {};
189
- const usd = deploy.packageUsda !== undefined ? usdJsonFromPackage(deploy.packageUsda) : undefined;
190
- const fromUsd = usd !== undefined ? stableIdsFromUsd(usd) : {};
191
143
  for (const [key, iface] of Object.entries(ifaces)) {
192
144
  const dom = evolveDomainFromInterface(iface);
193
- // Augment fields with drivers off the package USD-IR schemas (interface carries none).
194
- const usdDom = fromUsd[key];
195
- if (usdDom !== undefined) {
145
+ const locked = lock?.domains[key] ?? lock?.domains[iface.domain];
146
+ const stableIds = locked?.stableIds;
147
+ if (stableIds !== undefined) {
196
148
  for (const agg of Object.values(dom.aggregates)) {
197
- const stableAggregate = Object.values(usdDom.stableIds.aggregates)
149
+ const stableAggregate = Object.values(stableIds.aggregates)
198
150
  .find((candidate) => candidate.sid === agg.sid);
199
151
  const stableFields = new Map(
200
152
  Object.values(stableAggregate?.fields ?? {})
201
153
  .map((field) => [field.sid, field] as const),
202
154
  );
203
- const schema = usdDom.schemas[agg.label]
204
- ?? Object.entries(usdDom.schemas).find(([type]) => type.endsWith(`.${agg.label}`))?.[1];
155
+ const schema = locked?.schemas?.[agg.label]
156
+ ?? Object.entries(locked?.schemas ?? {}).find(([type]) => type.endsWith(`.${agg.label}`))?.[1];
205
157
  for (const f of Object.values(agg.fields)) {
206
158
  const driver = schema?.[f.label];
207
159
  if (driver !== undefined) (f as { driver?: string }).driver = JSON.stringify(driver);
@@ -216,10 +168,14 @@ export function evolveLawFromDeployJson(deploy: DeployJson): EvolveLaw {
216
168
  }
217
169
  law[key] = dom;
218
170
  }
219
- // A deploy.json with NO interface (older builds) still yields aggregate identity from the USD.
220
- for (const [key, sd] of Object.entries(fromUsd)) {
171
+ // A lockfile without a sibling interface still supports aggregate/field identity checks.
172
+ for (const [key, locked] of Object.entries(lock?.domains ?? {})) {
221
173
  if (law[key] !== undefined) continue;
222
- law[key] = { domainSid: sd.stableIds.domain, aggregates: aggregatesFromStableIds(sd.stableIds, sd.schemas), directives: {} };
174
+ law[key] = {
175
+ domainSid: locked.stableIds?.domain,
176
+ aggregates: aggregatesFromStableIds(locked.stableIds, locked.schemas),
177
+ directives: {},
178
+ };
223
179
  }
224
180
  return law;
225
181
  }
@@ -254,13 +210,19 @@ export function currentEvolveLaw(
254
210
  const stableIds = locked?.stableIds;
255
211
  const iface = domainInterface(mod, stableIds);
256
212
  const dom = evolveDomainFromInterface(iface);
257
- // Fold drivers from the lockfile schemas (interface has none).
258
- if (locked?.schemas !== undefined) {
213
+ // Fold drivers from the stable fields. Verbose v1 locks may also carry the
214
+ // older aggregate-schema copy; compact v2 locks deliberately omit it.
215
+ if (stableIds !== undefined) {
259
216
  for (const agg of Object.values(dom.aggregates)) {
260
- const schema = locked.schemas[agg.label];
261
- if (schema === undefined) continue;
217
+ const stableAggregate = Object.values(stableIds.aggregates).find(
218
+ (candidate) => candidate.sid === agg.sid,
219
+ );
220
+ const schema = locked?.schemas?.[agg.label];
262
221
  for (const f of Object.values(agg.fields)) {
263
- const driver = schema[f.label];
222
+ const stableField = Object.values(stableAggregate?.fields ?? {}).find(
223
+ (candidate) => candidate.sid === f.sid,
224
+ );
225
+ const driver = stableField?.driver ?? schema?.[f.label];
264
226
  if (driver !== undefined) (f as { driver?: string }).driver = JSON.stringify(driver);
265
227
  }
266
228
  }
@@ -273,7 +235,7 @@ export function currentEvolveLaw(
273
235
  // ── the diff ────────────────────────────────────────────────────────────────────────
274
236
 
275
237
  /** The full preview result: the findings AND the merged dispositions the destructive findings
276
- * demand, keyed by DOMAIN KEY (exactly the object a deploy JSON carries per installDomain). */
238
+ * demand, keyed by DOMAIN KEY (the object carried by an installDomain offer). */
277
239
  export interface EvolveDiffResult {
278
240
  readonly findings: Finding[];
279
241
  /** domain key → the dispositions fragment that makes THIS domain's destructive evolves admit. */
@@ -335,7 +297,7 @@ function diffDomain(
335
297
  domain,
336
298
  level: "refuse",
337
299
  what: `aggregate '${oldAgg.label}' (stable id ${sid}) is REMOVED — the evolve gate will refuse without a disposition`,
338
- fix: `paste into your deploy JSON: "dispositions": { "retired": ["${sid}"] }`,
300
+ fix: `include in the installation offer: "dispositions": { "retired": ["${sid}"] }`,
339
301
  });
340
302
  continue;
341
303
  }
@@ -357,7 +319,7 @@ function diffDomain(
357
319
  domain,
358
320
  level: "refuse",
359
321
  what: `field '${oldAgg.label}.${oldF.label}' (stable id ${fSid}) is REMOVED — the evolve gate will refuse without a disposition`,
360
- fix: `paste into your deploy JSON: "dispositions": { "retired": ["${fSid}"] }`,
322
+ fix: `include in the installation offer: "dispositions": { "retired": ["${fSid}"] }`,
361
323
  });
362
324
  continue;
363
325
  }
@@ -394,7 +356,7 @@ function diffDomain(
394
356
  domain,
395
357
  level: "refuse",
396
358
  what: `field '${oldAgg.label}.${oldF.label}' (stable id ${fSid}) is RETYPED in place (${how}) — the evolve gate will refuse without a disposition`,
397
- fix: `paste into your deploy JSON: "dispositions": { "retyped": { "${fSid}": "<why goes here>" } } — or add a NEW field instead`,
359
+ fix: `include in the installation offer: "dispositions": { "retyped": { "${fSid}": "<why goes here>" } } — or add a NEW field instead`,
398
360
  });
399
361
  }
400
362
  }
@@ -490,7 +452,7 @@ export function evolveDiff(prior: EvolveLaw, current: EvolveLaw): EvolveDiffResu
490
452
  domain,
491
453
  level: "refuse",
492
454
  what: `domain '${domain}' aggregate '${agg.label}' (stable id ${sid}) disappears — the current build no longer carries domain '${domain}'`,
493
- fix: `restore the domain, or (a true removal) redeploy with "dispositions": { "retired": ["${sid}"] }`,
455
+ fix: `restore the domain, or include "dispositions": { "retired": ["${sid}"] } in the replacement installation offer`,
494
456
  });
495
457
  }
496
458
  continue;
package/src/extremum.ts CHANGED
@@ -28,7 +28,7 @@
28
28
  * VALIDATION RULE (manifest-load, not type-level): the `valueField` parameter MUST be
29
29
  * an `int`-kind field of the `of`-aggregate. The DSL type system cannot enforce this
30
30
  * at the `min/max(id, valueField)` call site (the aggregate is not yet known), but
31
- * `AggregateSchemas::parse` (readmodel/src/manifest.rs) performs a no-fallback reject
31
+ * typed USDA projection-law admission performs a no-fallback reject
32
32
  * if the field is absent or non-int on the declared aggregate.
33
33
  *
34
34
  * ORDER-SENSITIVE GUARDRAIL: min/max expose NO `.first`/`.take`/`.orderBy`. min/max
package/src/fields.ts CHANGED
@@ -142,6 +142,74 @@ export interface EvidenceRefValue {
142
142
  storageClass: "git" | "external";
143
143
  }
144
144
 
145
+ /** One portable address inside Nomos custody. This is identity and routing, never authority. */
146
+ export interface NomosRefValue {
147
+ readonly workspace: string;
148
+ readonly type: string;
149
+ readonly id: string;
150
+ }
151
+
152
+ /** An immutable version pin plus the stable series whose law names the current version. */
153
+ export interface VersionedNomosRefValue {
154
+ readonly pinned: NomosRefValue;
155
+ readonly series: NomosRefValue;
156
+ }
157
+
158
+ /** A SHA-256 digest encoded as exactly 64 lowercase hexadecimal characters.
159
+ *
160
+ * This is nominal at author/client compile time and remains a plain string on the
161
+ * wire. Construct values through {@link sha256Digest}; never cast a string into it.
162
+ */
163
+ export type Sha256Digest = string & { readonly __nomosSha256Digest: unique symbol };
164
+
165
+ /** Codegen marker for the semantic SHA-256 string type. The marker is deliberately
166
+ * non-enumerable: Zod remains the runtime/admission schema, while emitters recover
167
+ * the stronger client type without adding a parallel manifest representation. */
168
+ export const NOMOS_SHA256_DIGEST_BRAND: unique symbol = Symbol.for("nomos.sha256Digest");
169
+
170
+ const SHA256_DIGEST_RE = /^[0-9a-f]{64}$/;
171
+
172
+ /** Declare and validate a lowercase hexadecimal SHA-256 digest payload value. */
173
+ export function sha256Digest(): z.ZodType<Sha256Digest> {
174
+ const schema = z.string().regex(SHA256_DIGEST_RE, "expected a lowercase hexadecimal SHA-256 digest");
175
+ Object.defineProperty(schema, NOMOS_SHA256_DIGEST_BRAND, { value: true, enumerable: false });
176
+ return schema as unknown as z.ZodType<Sha256Digest>;
177
+ }
178
+
179
+ /** Recover the SHA-256 semantic marker through omission/null wrappers. */
180
+ export function isSha256DigestSchema(schema: z.ZodTypeAny): boolean {
181
+ let current: z.ZodTypeAny | undefined = schema;
182
+ for (let hops = 0; current !== undefined && hops < 16; hops++) {
183
+ if ((current as unknown as Record<symbol, unknown>)[NOMOS_SHA256_DIGEST_BRAND] === true) {
184
+ return true;
185
+ }
186
+ const raw = current as unknown as {
187
+ _def?: { type?: unknown; innerType?: z.ZodTypeAny };
188
+ def?: { type?: unknown; innerType?: z.ZodTypeAny };
189
+ };
190
+ const def = raw._def ?? raw.def;
191
+ const kind = def?.type;
192
+ const name = (current as unknown as { _def?: { typeName?: string } })._def?.typeName;
193
+ if (kind !== "optional" && kind !== "default" && kind !== "nullable" &&
194
+ name !== "ZodOptional" && name !== "ZodDefault" && name !== "ZodNullable") {
195
+ return false;
196
+ }
197
+ current = def?.innerType;
198
+ }
199
+ return false;
200
+ }
201
+
202
+ export const nomosRefValueSchema: z.ZodType<NomosRefValue> = z.object({
203
+ workspace: z.string().min(1),
204
+ type: z.string().min(1),
205
+ id: z.string().min(1),
206
+ }).strict();
207
+
208
+ export const versionedNomosRefValueSchema: z.ZodType<VersionedNomosRefValue> = z.object({
209
+ pinned: nomosRefValueSchema,
210
+ series: nomosRefValueSchema,
211
+ }).strict();
212
+
145
213
  /** The Zod schema for an authored `EvidenceRefValue` — the single source of truth. */
146
214
  export const evidenceRefSchema: z.ZodType<EvidenceRefValue> = z.object({
147
215
  hash: z.string(),
@@ -174,6 +242,8 @@ export type ValueObjectFieldDescriptor =
174
242
  | {
175
243
  readonly type: "string";
176
244
  readonly brand?: string;
245
+ /** A framework semantic string whose generated client type validates its canonical spelling. */
246
+ readonly digest?: "sha256";
177
247
  readonly idOf?: string;
178
248
  /** Present only for an `idOf(recordContract(...))` target. Aggregate is the default. */
179
249
  readonly idOfContract?: true;
@@ -285,6 +355,9 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
285
355
  * absent the clear lane is ungated (and `law_check` WARNs if sibling writes ARE gated). Compose/codegen
286
356
  * metadata — the REAL law is the generated directive's `requiresRelationGate` (hashed there, not here). */
287
357
  readonly clearRequires?: string;
358
+ /** Static Zanzibar object paired with `clearRequires`. Non-workspace clear lanes must state this so
359
+ * generated directives cannot silently fall back to the wrong authorization namespace. */
360
+ readonly clearRequiresObject?: string;
288
361
  /** For `t.ref` cross-workspace edges: the foreign workspace id, if any. */
289
362
  readonly refWorkspace?: string;
290
363
  /**
@@ -306,6 +379,21 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
306
379
  * the target's concrete implementation. */
307
380
  readonly targetIsContract?: boolean;
308
381
  };
382
+ /** First-class atomic custody address. Kept in the reference kind so query/read routing remains a ref. */
383
+ readonly nomosRef?: {
384
+ readonly targetAggregateId: string;
385
+ readonly targetIsContract?: boolean;
386
+ };
387
+ /** Immutable publication pin + stable series, with the current pointer selected by compiled law. */
388
+ readonly versionedForeignRef?: {
389
+ readonly targetAggregateId: string;
390
+ readonly targetIsContract?: boolean;
391
+ readonly seriesAggregateId: string;
392
+ readonly seriesIsContract?: boolean;
393
+ readonly currentField: string;
394
+ /** Owning domain for cross-domain target, series and current-field law references. */
395
+ readonly targetDomain?: string;
396
+ };
309
397
  /** For `t.ref` (the target) AND `t.hasMany` (the child type): the related aggregate's wire id. */
310
398
  readonly refAggregateId?: string;
311
399
  /** For `t.hasMany(Child).via(backref)`: the back-reference field NAME on the child. Virtual. */
@@ -399,6 +487,8 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
399
487
  * Flag named `isMeta` — the `meta()` METHOD sets it, mirroring `encrypted()`/`optional()`.
400
488
  */
401
489
  readonly isMeta?: true;
490
+ /** ZD6 — this encrypted field carries a blind-index equality token (the `blindIndexed()` METHOD sets it). */
491
+ readonly isBlindIndexed?: true;
402
492
  /**
403
493
  * THE INTEGRITY/CONFIDENTIALITY FACET (security_model.md — the endorse model). Set by the
404
494
  * `authorityField`/`attributionField`/`standingField`/`secretField`/`publicField` wrappers (or the
@@ -418,6 +508,15 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
418
508
  withFacet<Fa extends FieldFacet>(facet: Fa): Field<T, K> & { readonly facet: Fa };
419
509
  /** Mark this field's value ENCRYPTED (sets {@link Field.isEncrypted}). Chainable + non-destructive. */
420
510
  encrypted(): Field<T, K>;
511
+ /**
512
+ * ZD6 BLIND INDEX (encrypted_merge_model.md — the CipherSweet pattern): opt THIS encrypted field
513
+ * into EDGE-SERVED EQUALITY lookups. The sealing client attaches a scope-key-derived HMAC token
514
+ * (`bidx`) beside the ciphertext; key holders compute the same token query-side; the operator can
515
+ * observe equality/frequency among the opted-in rows but can never invert a token. Requires
516
+ * `.encrypted()` (aggregate() refuses otherwise) — a DELIBERATE, DECLARED equality leak, rotated
517
+ * with the scope epoch. Sets {@link Field.isBlindIndexed}.
518
+ */
519
+ blindIndexed(): Field<T, K>;
421
520
  /** Keep AUTHOR PROVENANCE beside this field's folded value (sets {@link Field.isAttributed}) —
422
521
  * `<field>Provenance` accessors are generated on the read models. Lww-class scalars only. */
423
522
  attributed(): Field<T, K>;
@@ -441,7 +540,7 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
441
540
  withNull(semantic: NullSemantic, reason?: string): Field<T, K>;
442
541
  /** INTERNAL (`t.clearable` calls this): mark this field EXPLICIT-CLEARABLE (sets {@link Field.isClearable}
443
542
  * + optional {@link Field.clearRequires}). Threaded through merge/optional like every other flag. */
444
- clearable(requires?: string): Field<T, K> & { readonly isClearable: true };
543
+ clearable(requires?: string, object?: string): Field<T, K> & { readonly isClearable: true };
445
544
  /** Declare how values authored by an earlier field contract become this field's current
446
545
  * business value. With no function, the compiler proves identity widening. With a function,
447
546
  * the source must be finite (enum/boolean/literal union), so Nomos can exhaustively compile
@@ -458,6 +557,14 @@ function makeField<T, K extends FieldKind = FieldKind>(
458
557
  extra: {
459
558
  refWorkspace?: string;
460
559
  foreignRef?: { workspaceField: string; targetAggregateId: string; targetDomain?: string; targetIsContract?: boolean };
560
+ nomosRef?: { targetAggregateId: string; targetIsContract?: boolean };
561
+ versionedForeignRef?: {
562
+ targetAggregateId: string;
563
+ targetIsContract?: boolean;
564
+ seriesAggregateId: string;
565
+ seriesIsContract?: boolean;
566
+ currentField: string;
567
+ };
461
568
  refAggregateId?: string;
462
569
  /** LAZY target resolution — `t.ref(() => Agg)` / `t.hasMany(() => Agg)`. Breaks a BIDIRECTIONAL
463
570
  * declaration cycle (a Parent's `hasMany(Child)` ⇄ a Child's `ref(Parent)`): the thunk is stored and
@@ -473,6 +580,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
473
580
  valueObject?: ValueObjectFieldMeta;
474
581
  mapValueVo?: ValueObjectFieldMeta;
475
582
  encrypted?: true;
583
+ blindIndexed?: true;
476
584
  attributed?: true;
477
585
  meta?: true;
478
586
  facet?: FieldFacet;
@@ -480,6 +588,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
480
588
  nullReason?: string;
481
589
  isClearable?: true;
482
590
  clearRequires?: string;
591
+ clearRequiresObject?: string;
483
592
  evolutions?: readonly FieldEvolutionDeclaration[];
484
593
  } = {},
485
594
  ): Field<T, K> {
@@ -493,12 +602,15 @@ function makeField<T, K extends FieldKind = FieldKind>(
493
602
  ...(extra.refWorkspace !== undefined ? { refWorkspace: extra.refWorkspace } : {}),
494
603
  ...(extra.refAggregateId !== undefined ? { refAggregateId: extra.refAggregateId } : {}),
495
604
  ...(extra.foreignRef !== undefined ? { foreignRef: extra.foreignRef } : {}),
605
+ ...(extra.nomosRef !== undefined ? { nomosRef: extra.nomosRef } : {}),
606
+ ...(extra.versionedForeignRef !== undefined ? { versionedForeignRef: extra.versionedForeignRef } : {}),
496
607
  ...(extra.viaField !== undefined ? { viaField: extra.viaField } : {}),
497
608
  ...(extra.mapValueKind !== undefined ? { mapValueKind: extra.mapValueKind } : {}),
498
609
  ...(extra.jsonShape !== undefined ? { jsonShape: extra.jsonShape } : {}),
499
610
  ...(extra.valueObject !== undefined ? { valueObject: extra.valueObject } : {}),
500
611
  ...(extra.mapValueVo !== undefined ? { mapValueVo: extra.mapValueVo } : {}),
501
612
  ...(extra.encrypted ? { isEncrypted: true as const } : {}),
613
+ ...(extra.blindIndexed ? { isBlindIndexed: true as const } : {}),
502
614
  ...(extra.attributed ? { isAttributed: true as const } : {}),
503
615
  ...(extra.meta ? { isMeta: true as const } : {}),
504
616
  ...(extra.fromBirth ? { isFromBirth: true as const } : {}),
@@ -506,6 +618,7 @@ function makeField<T, K extends FieldKind = FieldKind>(
506
618
  ...(extra.nullReason !== undefined ? { nullReason: extra.nullReason } : {}),
507
619
  ...(extra.isClearable ? { isClearable: true as const } : {}),
508
620
  ...(extra.clearRequires !== undefined ? { clearRequires: extra.clearRequires } : {}),
621
+ ...(extra.clearRequiresObject !== undefined ? { clearRequiresObject: extra.clearRequiresObject } : {}),
509
622
  ...(extra.evolutions !== undefined && extra.evolutions.length > 0
510
623
  ? { evolutions: extra.evolutions }
511
624
  : {}),
@@ -549,11 +662,12 @@ function makeField<T, K extends FieldKind = FieldKind>(
549
662
  ...(reason !== undefined ? { nullReason: reason } : {}),
550
663
  });
551
664
  },
552
- clearable(requires?: string): Field<T, K> & { readonly isClearable: true } {
665
+ clearable(requires?: string, object?: string): Field<T, K> & { readonly isClearable: true } {
553
666
  return makeField<T, K>(kind, zod, driver, {
554
667
  ...extra,
555
668
  isClearable: true,
556
669
  ...(requires !== undefined ? { clearRequires: requires } : {}),
670
+ ...(object !== undefined ? { clearRequiresObject: object } : {}),
557
671
  }) as Field<T, K> & { readonly isClearable: true };
558
672
  },
559
673
  evolvesFrom<From>(source: Field<From>, project?: (value: From) => T): Field<T, K> {
@@ -600,6 +714,11 @@ function makeField<T, K extends FieldKind = FieldKind>(
600
714
  // `nomosEncryption` era key, built by aggregate()/manifest, is what carries the fact.
601
715
  return makeField<T, K>(kind, zod, driver, { ...extra, encrypted: true });
602
716
  },
717
+ blindIndexed(): Field<T, K> {
718
+ // ZD6 — carried through merge/optional like encrypted(); aggregate() enforces the
719
+ // encrypted-only rule (a plaintext field needs no blind index — index it directly).
720
+ return makeField<T, K>(kind, zod, driver, { ...extra, blindIndexed: true });
721
+ },
603
722
  attributed(): Field<T, K> {
604
723
  // Carry the flag through merge/optional (extra is threaded), so `.merge(Lww).attributed()`
605
724
  // and `.attributed().merge(Lww)` agree. aggregate() validates Lww-class eligibility and
@@ -771,6 +890,11 @@ export const t = {
771
890
  }
772
891
  return makeField("string", schema, FirstWriterWins);
773
892
  },
893
+ /** A SHA-256 digest with one canonical wire spelling: 64 lowercase hex characters.
894
+ * Generated clients surface [Sha256Digest], not a bare string. */
895
+ sha256Digest(): Field<Sha256Digest, "string"> {
896
+ return makeField<Sha256Digest, "string">("string", sha256Digest(), FirstWriterWins);
897
+ },
774
898
  int(rules: IntRules = {}): Field<number, "int"> {
775
899
  for (const [name, value] of [["minimum", rules.minimum], ["maximum", rules.maximum]] as const) {
776
900
  if (value !== undefined && !Number.isSafeInteger(value)) {
@@ -904,10 +1028,13 @@ export const t = {
904
1028
  */
905
1029
  clearable<T, K extends FieldKind>(
906
1030
  field: Field<T, K>,
907
- opts?: { requires?: string },
1031
+ opts?: { requires?: string; object?: string },
908
1032
  ): Field<T, K> & { readonly isClearable: true } {
909
1033
  assertNullEligible(field, "clearable");
910
- return field.clearable(opts?.requires);
1034
+ if (opts?.object !== undefined && opts.requires === undefined) {
1035
+ throw new Error("t.clearable(..., { object }) also requires an authorization relation");
1036
+ }
1037
+ return field.clearable(opts?.requires, opts?.object);
911
1038
  },
912
1039
  /**
913
1040
  * `t.nullable(field, { reason })` — null IS a real domain value on this field (`architecture/
@@ -977,6 +1104,58 @@ export const t = {
977
1104
  },
978
1105
  });
979
1106
  },
1107
+ /** A first-class, atomic portable custody address. Unlike legacy `foreignRef`, there is no sibling
1108
+ * workspace field which can tear under merge; workspace, aggregate type and id are one Lww value. */
1109
+ nomosRef(
1110
+ target: AggregateHandle<string, Record<string, Field>> | ContractHandle,
1111
+ ): Field<NomosRefValue, "ref"> {
1112
+ const isContract = (target as { __isContract?: unknown }).__isContract === true;
1113
+ return makeField<NomosRefValue, "ref">("ref", nomosRefValueSchema, FirstWriterWins, {
1114
+ refAggregateId: target.id,
1115
+ nomosRef: {
1116
+ targetAggregateId: target.id,
1117
+ ...(isContract ? { targetIsContract: true } : {}),
1118
+ },
1119
+ });
1120
+ },
1121
+ /** A version pin whose freshness is checked against one law-declared current pointer on a stable series.
1122
+ * The declaration fails immediately if `series.current` is not an atomic ref to `target`. */
1123
+ versionedForeignRef(opts: {
1124
+ target: AggregateHandle<string, Record<string, Field>> | ContractHandle;
1125
+ series: AggregateHandle<string, Record<string, Field>> | ContractHandle;
1126
+ current: string;
1127
+ domain?: string;
1128
+ }): Field<VersionedNomosRefValue, "ref"> {
1129
+ const current = (opts.series.fields as Record<string, Field>)[opts.current];
1130
+ if (current === undefined) {
1131
+ throw new Error(
1132
+ `t.versionedForeignRef: series '${opts.series.id}' has no current field '${opts.current}'`,
1133
+ );
1134
+ }
1135
+ if (current.nomosRef?.targetAggregateId !== opts.target.id) {
1136
+ throw new Error(
1137
+ `t.versionedForeignRef: '${opts.series.id}.${opts.current}' must be ` +
1138
+ `t.nomosRef(${opts.target.id}); it is the O(1) current pointer used for freshness`,
1139
+ );
1140
+ }
1141
+ const targetIsContract = (opts.target as { __isContract?: unknown }).__isContract === true;
1142
+ const seriesIsContract = (opts.series as { __isContract?: unknown }).__isContract === true;
1143
+ return makeField<VersionedNomosRefValue, "ref">(
1144
+ "ref",
1145
+ versionedNomosRefValueSchema,
1146
+ FirstWriterWins,
1147
+ {
1148
+ versionedForeignRef: {
1149
+ targetAggregateId: opts.target.id,
1150
+ ...(targetIsContract ? { targetIsContract: true } : {}),
1151
+ seriesAggregateId: opts.series.id,
1152
+ ...(seriesIsContract ? { seriesIsContract: true } : {}),
1153
+ currentField: opts.current,
1154
+ ...(opts.domain === undefined ? {} : { targetDomain: opts.domain }),
1155
+ },
1156
+ },
1157
+ );
1158
+ },
980
1159
  /**
981
1160
  * A 1:N INVERSE relation — the read side of a `t.ref` on the child (`aggregate_lifecycle_and_relations.md`).
982
1161
  * `t.hasMany(Child).via("parent")` on a Parent declares "the Children whose `parent` ref points here".
package/src/flow_gate.ts CHANGED
@@ -15,7 +15,11 @@
15
15
  // Integrity (who may WRITE) is gated natively at admission by `.requires(relation)`;
16
16
  // the integrity-flow dual + rule 4 (secret-correlated refusal) + `declassify` + the
17
17
  // `nomosFlowGate` era key are the next slices.
18
- import type { CanonicalManifest } from "./manifest.js";
18
+ import type { DomainModule } from "./codegen_dart.js";
19
+ import { finishCount } from "./count.js";
20
+ import { existsAsCount } from "./exists.js";
21
+ import { finishExtremum } from "./extremum.js";
22
+ import { finishSum } from "./sum.js";
19
23
  import type { CanonicalPred } from "./predicate.js";
20
24
 
21
25
  /** A read projection, read structurally — every read kind (query / count / sum / extremum /
@@ -32,6 +36,26 @@ type Projection = {
32
36
  readonly where?: CanonicalPred;
33
37
  };
34
38
 
39
+ /** The finite facts the information-flow proof consumes. A canonical manifest satisfies this
40
+ * structurally for legacy callers; the production compiler derives it straight from typed declarations. */
41
+ export interface FlowLaw {
42
+ readonly aggregates: readonly {
43
+ readonly id: string;
44
+ readonly encryptedFields?: readonly string[];
45
+ readonly requiresRead?: string;
46
+ }[];
47
+ readonly directives: readonly {
48
+ readonly id: string;
49
+ readonly capturedReads?: readonly { readonly queryId: string; readonly returns: string }[];
50
+ }[];
51
+ readonly queries?: readonly Projection[];
52
+ readonly counts?: readonly Projection[];
53
+ readonly sums?: readonly Projection[];
54
+ readonly mins?: readonly Projection[];
55
+ readonly maxes?: readonly Projection[];
56
+ readonly orderedReads?: readonly Projection[];
57
+ }
58
+
35
59
  /** The raw module bits rule 4 needs that the canonical manifest strips (the executable
36
60
  * invariant `.reads` + the declassify waivers). Read structurally — no coupling to DomainModule. */
37
61
  type FlowModule = {
@@ -82,7 +106,7 @@ function predFields(p: CanonicalPred | undefined): string[] {
82
106
  * {@link FlowGateError} on the first violation (a typed compile error). Pure over
83
107
  * manifest IR — no side effects, no AST.
84
108
  */
85
- export function assertFlowGate(m: CanonicalManifest, mod?: FlowModule): void {
109
+ export function assertFlowGate(m: FlowLaw, mod?: FlowModule): void {
86
110
  // ── RULE 1 — NO ENCRYPT-THEN-INDEX ──────────────────────────────────────────
87
111
  // A field that is .encrypted() (operator-blind ciphertext) MUST NOT appear in any
88
112
  // read's group-by / key / value / where over its own type: grouping/indexing/
@@ -233,3 +257,52 @@ export function assertFlowGate(m: CanonicalManifest, mod?: FlowModule): void {
233
257
  }
234
258
  }
235
259
  }
260
+
261
+ /**
262
+ * Production compiler entry: prove information flow directly from the live typed declarations.
263
+ * This deliberately does not build, serialize or hash the retired canonical JSON manifest.
264
+ */
265
+ export function assertFlowGateModule(mod: DomainModule): void {
266
+ const counts = [
267
+ ...(mod.counts ?? []).map(finishCount),
268
+ ...(mod.exists_ ?? []).map(existsAsCount),
269
+ ];
270
+ const facts: FlowLaw = {
271
+ aggregates: mod.aggregates.map((aggregate) => ({
272
+ id: aggregate.id,
273
+ ...(aggregate.encryptedFields === undefined ? {} : { encryptedFields: aggregate.encryptedFields }),
274
+ ...(aggregate.visibility === undefined ? {} : { requiresRead: aggregate.visibility.requires }),
275
+ })),
276
+ directives: mod.directives.map((directive) => ({
277
+ id: directive.id,
278
+ ...((directive.declaredQueryReads ?? []).length === 0
279
+ ? {}
280
+ : {
281
+ capturedReads: (directive.declaredQueryReads ?? []).map((read) => ({
282
+ queryId: read.id,
283
+ returns: read.returns,
284
+ })),
285
+ }),
286
+ })),
287
+ ...(mod.queries === undefined ? {} : { queries: mod.queries }),
288
+ ...(counts.length === 0 ? {} : { counts }),
289
+ ...(mod.sums === undefined
290
+ ? {}
291
+ : {
292
+ sums: mod.sums.map((raw) => {
293
+ const sum = finishSum(raw);
294
+ return {
295
+ id: sum.id,
296
+ of: sum.of,
297
+ valueField: sum.sumField,
298
+ ...(sum.by === undefined ? {} : { by: sum.by }),
299
+ ...(sum.where === undefined ? {} : { where: sum.where }),
300
+ };
301
+ }),
302
+ }),
303
+ ...(mod.mins === undefined ? {} : { mins: mod.mins.map(finishExtremum) }),
304
+ ...(mod.maxes === undefined ? {} : { maxes: mod.maxes.map(finishExtremum) }),
305
+ ...(mod.orderedReads === undefined ? {} : { orderedReads: mod.orderedReads }),
306
+ };
307
+ assertFlowGate(facts, mod);
308
+ }
@@ -38,6 +38,7 @@ import {
38
38
  seedBirthAttestation,
39
39
  seedReceivingGroup,
40
40
  seedReceivingGroupPolicy,
41
+ sharedByObject,
41
42
  sharedByWorkspace,
42
43
  } from "./home.js";
43
44
  import { RelationTuple, relationTuplesByObject } from "./relations.js";
@@ -86,6 +87,7 @@ export const __nomos_SharedWithMe = aggregate(SharedWithMe);
86
87
  export const __nomos_recordShare = directive(recordShare);
87
88
  export const __nomos_pruneShare = directive(pruneShare);
88
89
  export const __nomos_sharedByWorkspace = query(sharedByWorkspace);
90
+ export const __nomos_sharedByObject = query(sharedByObject);
89
91
 
90
92
  // Warrant, provenance, signer seating, recovery and verifier state.
91
93
  export const __nomos_GenesisWarrant = aggregate(GenesisWarrant);