@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.
- package/compile_package.mjs +43 -2
- package/compile_proof.d.mts +47 -0
- package/compile_proof.mjs +343 -0
- package/dart/CHANGELOG.md +7 -0
- package/dart/lib/src/dev_cloud.dart +8 -8
- package/dart/lib/src/nms_generated_runtime.dart +2 -0
- package/dart/lib/src/runner/node-entry.mjs +5504 -1236
- package/dart/lib/src/runner/offline-install.mjs +4578 -528
- package/dart/lib/src/test_harness.dart +206 -152
- package/dart/lib/src/webview_bridge.dart +196 -89
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +4 -3
- package/dart/test/adv_realm_interference_test.dart +7 -6
- package/dart/test/cloud_bound_topology_test.dart +3 -3
- package/dart/test/cluster_births_test.dart +4 -3
- package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
- package/dart/test/cluster_local_autosync_test.dart +3 -2
- package/dart/test/cluster_scale_test.dart +3 -2
- package/dart/test/dlq_session_surface_test.dart +7 -6
- package/dart/test/first_app_gauntlet_test.dart +14 -21
- package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
- package/dart/test/four_layer_nested_topology_test.dart +4 -4
- package/dart/test/harness_phase_timeout_test.dart +2 -2
- package/dart/test/local_sync_inert_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +2 -2
- package/dart/test/sealing_client_test.dart +4 -3
- package/dart/test/session_bound_test.dart +8 -7
- package/dart/test/test_harness_test.dart +13 -12
- package/dart/test/vo_read_typing_test.dart +4 -3
- package/dart/test/watch_after_offer_stall_test.dart +4 -3
- package/dart/test/webview_bridge_test.dart +102 -5
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/nomos_types.dart +9 -0
- package/dart_types/lib/src/nomos_ref.dart +20 -0
- package/dart_types/lib/src/optional_ref.dart +85 -0
- package/dart_types/lib/src/sha256_digest.dart +41 -0
- package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/optional_ref_test.dart +44 -0
- package/dart_types/test/sha256_digest_test.dart +18 -0
- package/package.json +9 -5
- package/runtime/generated-application-host.js +5365 -1105
- package/src/aggregate.ts +18 -0
- package/src/application_composition.ts +6 -32
- package/src/attested_core.ts +6 -3
- package/src/birth_meter.ts +1 -0
- package/src/birth_meter_ensure.ts +28 -2
- package/src/births.ts +1 -1
- package/src/build_package.ts +162 -538
- package/src/business_birth.ts +19 -3
- package/src/canonical_json.ts +21 -0
- package/src/check_package_main.ts +76 -35
- package/src/clear_directive.ts +19 -8
- package/src/codegen_dart.ts +305 -22
- package/src/codegen_dart_application.ts +844 -292
- package/src/codegen_dart_client.ts +1084 -407
- package/src/codegen_dart_scenario.ts +12 -12
- package/src/codegen_proof.ts +169 -39
- package/src/codegen_provider_dart.ts +17 -1
- package/src/codegen_ts.ts +298 -106
- package/src/compat_manifest.ts +2 -2
- package/src/compat_package_main.ts +8 -12
- package/src/compile_package_main.ts +1866 -597
- package/src/compose.ts +1 -1
- package/src/directive.ts +208 -15
- package/src/engine_entry.ts +165 -111
- package/src/evolve_preview.ts +43 -81
- package/src/extremum.ts +1 -1
- package/src/fields.ts +183 -4
- package/src/flow_gate.ts +75 -2
- package/src/framework/application_home.ts +2 -0
- package/src/framework/auth_provider_law.ts +2 -2
- package/src/framework/birth_meter.ts +7 -19
- package/src/framework/bootstrap.ts +0 -2
- package/src/framework/business_corrections.ts +9 -5
- package/src/framework/domain_lifecycle.ts +15 -29
- package/src/framework/encryption_scope.ts +2 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +16 -14
- package/src/framework/home.ts +20 -14
- package/src/framework/owned_custody.ts +9 -3
- package/src/framework/platform_admin.ts +3 -3
- package/src/framework/relations.ts +21 -13
- package/src/framework/signing_identity.ts +45 -40
- package/src/framework/workspace_invariant.ts +245 -0
- package/src/framework/workspaces.ts +23 -29
- package/src/index.ts +36 -16
- package/src/intent.ts +178 -6
- package/src/law_check.ts +32 -11
- package/src/law_graph.ts +2902 -0
- package/src/law_graph_usda.ts +1799 -0
- package/src/law_identity.ts +435 -0
- package/src/law_value_type.ts +230 -0
- package/src/lsp_server.ts +61 -31
- package/src/ops.ts +7 -0
- package/src/package_config.ts +141 -0
- package/src/package_dependencies.ts +108 -0
- package/src/package_dependency_usage.ts +150 -0
- package/src/package_source_imports.ts +71 -0
- package/src/plan_source_slice.ts +335 -0
- package/src/point_guard.ts +192 -0
- package/src/precondition.ts +44 -9
- package/src/read_projection_namespace.ts +1 -1
- package/src/runtime_zod.ts +44 -0
- package/src/secondary_ensure.ts +67 -14
- package/src/shape_compat.ts +1 -1
- package/src/stable_id_identity_bridge.ts +149 -0
- package/src/stable_id_lockfile.ts +278 -0
- package/src/stable_ids.ts +7 -2
- package/src/stable_ids_for_module.ts +348 -0
- package/src/stable_ids_types.ts +1 -1
- package/src/sum.ts +1 -1
- package/src/usd_state.ts +1 -1
- package/src/usda_relationship_integrity.ts +66 -0
- package/src/usda_text.ts +18 -0
- package/src/usdz.ts +78 -0
- package/src/value_object.ts +95 -0
- package/src/wire.ts +19 -0
- package/src/wire_encode.ts +22 -0
- package/src/workspace_sharding.ts +52 -36
- package/src/workspace_sharing.ts +28 -4
- package/src/zod_introspection.ts +1 -1
- package/dart/test/cluster_attested_test.dart +0 -201
- package/src/compile_engine.ts +0 -595
- package/src/manifest.ts +0 -1476
- package/src/read_projection_composition.ts +0 -117
- package/src/read_projection_manifest.ts +0 -115
- package/src/relation_composition.ts +0 -286
- package/src/usd.ts +0 -1967
- package/src/usd_layers.ts +0 -1776
package/src/codegen_ts.ts
CHANGED
|
@@ -43,7 +43,7 @@ import {
|
|
|
43
43
|
OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
|
|
44
44
|
type AggregateHandle,
|
|
45
45
|
} from "./aggregate.js";
|
|
46
|
-
import type
|
|
46
|
+
import { isSha256DigestSchema, type Field, type ValueObjectDescriptor, type ValueObjectFieldDescriptor } from "./fields.js";
|
|
47
47
|
import type { Directive } from "./directive.js";
|
|
48
48
|
import type { DomainModule } from "./codegen_dart.js";
|
|
49
49
|
import { OWNED_DEVICE_APPROVAL_DIRECTIVE, ownedRecordActionDirectiveId } from "./framework/owned_custody.js";
|
|
@@ -51,7 +51,7 @@ import { BUSINESS_CORRECTION_DIRECTIVE } from "./framework/business_corrections.
|
|
|
51
51
|
import { collectActivityDescriptors, type PackageActivityEntry } from "./codegen_dart.js";
|
|
52
52
|
import type { NomosCompat } from "./compat_manifest.js";
|
|
53
53
|
import type { QueryDecl } from "./query.js";
|
|
54
|
-
import { voMetaOf, collectPayloadValueObjectDescriptors, idOfMetaOf, idOfTargetKindOf } from "./value_object.js";
|
|
54
|
+
import { voMetaOf, collectPayloadValueObjectDescriptors, idOfMetaOf, idOfTargetKindOf, versionedRefOfMetaOf } from "./value_object.js";
|
|
55
55
|
import { recursiveTypeBaseName, zodUnwrapLazy } from "./zod_lazy.js";
|
|
56
56
|
import { finishCount } from "./count.js";
|
|
57
57
|
import { finishSum } from "./sum.js";
|
|
@@ -119,7 +119,7 @@ const tsOptAccess = (name: string): string =>
|
|
|
119
119
|
/^[A-Za-z_$][A-Za-z0-9_$]*$/.test(name) ? `.${name}` : `.[${JSON.stringify(name)}]`;
|
|
120
120
|
|
|
121
121
|
// ── NAME NORMALIZATION (no naming convention to learn) ────────────────────────────
|
|
122
|
-
// `nomos.
|
|
122
|
+
// `nomos.project.ts` names and domain keys may be ANY string — kebab-case,
|
|
123
123
|
// snake_case, space-separated, PascalCase. Artifact FILENAMES keep the raw name
|
|
124
124
|
// verbatim; generated SYMBOLS normalize it:
|
|
125
125
|
|
|
@@ -308,6 +308,17 @@ function tsTypeOfZod(
|
|
|
308
308
|
reg?: LazyTypeRegistry,
|
|
309
309
|
path = "",
|
|
310
310
|
): string {
|
|
311
|
+
if (isSha256DigestSchema(zt)) return "Sha256Digest";
|
|
312
|
+
const versioned = versionedRefOfMetaOf(zt);
|
|
313
|
+
if (versioned !== undefined) {
|
|
314
|
+
const target = versioned.targetKind === "contract"
|
|
315
|
+
? contractTypeName(versioned.targetAggregateType)
|
|
316
|
+
: versioned.targetAggregateType;
|
|
317
|
+
const series = versioned.seriesKind === "contract"
|
|
318
|
+
? contractTypeName(versioned.seriesAggregateType)
|
|
319
|
+
: versioned.seriesAggregateType;
|
|
320
|
+
return `VersionedNomosRef<${target}, ${series}>`;
|
|
321
|
+
}
|
|
311
322
|
if (recipientMetaOf(zt) !== undefined) return "Recipient<User>";
|
|
312
323
|
const aggregateType = idOfMetaOf(zt);
|
|
313
324
|
if (aggregateType !== undefined) {
|
|
@@ -339,6 +350,8 @@ function tsTypeOfZod(
|
|
|
339
350
|
return "number"; // `.int()` is a runtime constraint; TS has one number type
|
|
340
351
|
case "boolean":
|
|
341
352
|
return "boolean";
|
|
353
|
+
case "null":
|
|
354
|
+
return "null";
|
|
342
355
|
case "enum":
|
|
343
356
|
return union(zodEnumValues(zt).map((v) => JSON.stringify(v)));
|
|
344
357
|
case "literal": {
|
|
@@ -471,7 +484,7 @@ function tsTypeOfPayload(
|
|
|
471
484
|
reg?: LazyTypeRegistry,
|
|
472
485
|
pathPrefix = "",
|
|
473
486
|
): string {
|
|
474
|
-
if (zodKind(schema) !== "object"
|
|
487
|
+
if (zodKind(schema) !== "object") {
|
|
475
488
|
return tsTypeOfZod(schema, "", reg, pathPrefix);
|
|
476
489
|
}
|
|
477
490
|
const stamped = new Set(autoStamped);
|
|
@@ -485,8 +498,10 @@ function tsTypeOfPayload(
|
|
|
485
498
|
.map(([name, raw]) => {
|
|
486
499
|
let f = raw as z.ZodTypeAny;
|
|
487
500
|
let optional = stamped.has(name) || mintedSet.has(name) || placedSet.has(name);
|
|
501
|
+
let schemaOptional = false;
|
|
488
502
|
while (zodKind(f) === "optional" || zodKind(f) === "default") {
|
|
489
503
|
optional = true;
|
|
504
|
+
schemaOptional = true;
|
|
490
505
|
f = zodDef(f).innerType!;
|
|
491
506
|
}
|
|
492
507
|
const note = stamped.has(name)
|
|
@@ -495,6 +510,13 @@ function tsTypeOfPayload(
|
|
|
495
510
|
? " // picked LEAST-LOADED over the coordinator's law-state at offer when omitted (sharding §3)"
|
|
496
511
|
: "";
|
|
497
512
|
const fieldPath = pathPrefix === "" ? name : `${pathPrefix}.${name}`;
|
|
513
|
+
const optionalReferenceTarget = schemaOptional ? idOfMetaOf(raw as z.ZodTypeAny) : undefined;
|
|
514
|
+
if (optionalReferenceTarget !== undefined) {
|
|
515
|
+
const target = idOfTargetKindOf(raw as z.ZodTypeAny) === "contract"
|
|
516
|
+
? contractTypeName(optionalReferenceTarget)
|
|
517
|
+
: optionalReferenceTarget;
|
|
518
|
+
return ` ${name}: NomosOptionalRef<${target}>; // required choice: absent or present`;
|
|
519
|
+
}
|
|
498
520
|
return ` ${name}${optional ? "?" : ""}: ${tsTypeOfZod(f, " ", reg, fieldPath)};${note}`;
|
|
499
521
|
});
|
|
500
522
|
return fields.length ? `{\n${fields.join("\n")}\n}` : "Record<string, never>";
|
|
@@ -522,7 +544,7 @@ function tsTypeOfField(field: Field): string {
|
|
|
522
544
|
function tsBaseTypeOfField(field: Field): string {
|
|
523
545
|
switch (field.kind) {
|
|
524
546
|
case "string":
|
|
525
|
-
return "string";
|
|
547
|
+
return isSha256DigestSchema(field.zod as z.ZodTypeAny) ? "Sha256Digest" : "string";
|
|
526
548
|
case "int":
|
|
527
549
|
return "number";
|
|
528
550
|
// STRUCTURAL CRDT scalars (read-collapsed): a counter reads as its net number, a position as its
|
|
@@ -543,6 +565,16 @@ function tsBaseTypeOfField(field: Field): string {
|
|
|
543
565
|
// Both reference shapes keep aggregate identity typed. A same-workspace edge is the canonical
|
|
544
566
|
// NomosRef<TAggregate>; a foreign edge adds its decoder/read helpers.
|
|
545
567
|
const fr = (field as { foreignRef?: { targetAggregateId: string; targetIsContract?: boolean } }).foreignRef;
|
|
568
|
+
const vr = field.versionedForeignRef;
|
|
569
|
+
if (vr !== undefined) {
|
|
570
|
+
const target = vr.targetIsContract === true
|
|
571
|
+
? contractTypeName(vr.targetAggregateId)
|
|
572
|
+
: vr.targetAggregateId;
|
|
573
|
+
const series = vr.seriesIsContract === true
|
|
574
|
+
? contractTypeName(vr.seriesAggregateId)
|
|
575
|
+
: vr.seriesAggregateId;
|
|
576
|
+
return `VersionedNomosRef<${target}, ${series}>`;
|
|
577
|
+
}
|
|
546
578
|
// A contract target types against the shared `<ContractId>Contract` interface (the public DTO),
|
|
547
579
|
// not the concrete `<Agg>Data` — that is how the reference stays acyclic.
|
|
548
580
|
return fr !== undefined
|
|
@@ -599,7 +631,7 @@ function tsTypeOfVoField(f: ValueObjectFieldDescriptor): string {
|
|
|
599
631
|
case "string":
|
|
600
632
|
return f.idOf !== undefined
|
|
601
633
|
? `NomosRef<${f.idOfContract === true ? contractTypeName(f.idOf) : f.idOf}>`
|
|
602
|
-
: f.
|
|
634
|
+
: f.digest === "sha256" ? "Sha256Digest" : f.brand ?? "string";
|
|
603
635
|
case "number":
|
|
604
636
|
return "number"; // `.int()`/units are Dart-side nominal products; TS keeps one number
|
|
605
637
|
case "boolean":
|
|
@@ -699,9 +731,9 @@ function aggregatesOf(mod: Record<string, unknown>): AggregateHandle[] {
|
|
|
699
731
|
// ── the generator ─────────────────────────────────────────────────────────────────
|
|
700
732
|
|
|
701
733
|
export interface TsClientOptions {
|
|
702
|
-
/** The package name (`nomos.
|
|
734
|
+
/** The package name (`nomos.project.ts` `name`) — used for the hash constant. */
|
|
703
735
|
readonly packageName: string;
|
|
704
|
-
/**
|
|
736
|
+
/** Content identity of the built typed USDA closure (`.law.usdz`). */
|
|
705
737
|
readonly domainHash: string;
|
|
706
738
|
/** The package's STRUCTURAL INTERFACE (#72) — directives (payload shapes) + aggregates
|
|
707
739
|
* (read shapes, sid-keyed), keyed by domain. Baked in so `connect()` can negotiate
|
|
@@ -1024,6 +1056,36 @@ function anyPackedTaxonomy(modules: readonly DomainModule[]): boolean {
|
|
|
1024
1056
|
return false;
|
|
1025
1057
|
}
|
|
1026
1058
|
|
|
1059
|
+
/** Whether a schema contains the framework SHA-256 semantic string, including nested
|
|
1060
|
+
* value objects/arrays. Used only to keep its declaration omit-when-unused. */
|
|
1061
|
+
function zodUsesSha256Digest(schema: z.ZodTypeAny, seen = new Set<z.ZodTypeAny>()): boolean {
|
|
1062
|
+
if (seen.has(schema)) return false;
|
|
1063
|
+
seen.add(schema);
|
|
1064
|
+
if (isSha256DigestSchema(schema)) return true;
|
|
1065
|
+
switch (zodKind(schema)) {
|
|
1066
|
+
case "optional":
|
|
1067
|
+
case "default":
|
|
1068
|
+
case "nullable": {
|
|
1069
|
+
const inner = zodDef(schema).innerType;
|
|
1070
|
+
return inner !== undefined && zodUsesSha256Digest(inner, seen);
|
|
1071
|
+
}
|
|
1072
|
+
case "array":
|
|
1073
|
+
return zodUsesSha256Digest(zodArrayElement(schema), seen);
|
|
1074
|
+
case "object":
|
|
1075
|
+
return Object.values(zodObjectShape(schema)).some((field) => zodUsesSha256Digest(field, seen));
|
|
1076
|
+
case "record": {
|
|
1077
|
+
const value = zodDef(schema).valueType;
|
|
1078
|
+
return value !== undefined && zodUsesSha256Digest(value, seen);
|
|
1079
|
+
}
|
|
1080
|
+
case "union":
|
|
1081
|
+
return (zodDef(schema).options ?? []).some((option) => zodUsesSha256Digest(option, seen));
|
|
1082
|
+
case "lazy":
|
|
1083
|
+
return zodUsesSha256Digest(zodUnwrapLazy(schema), seen);
|
|
1084
|
+
default:
|
|
1085
|
+
return false;
|
|
1086
|
+
}
|
|
1087
|
+
}
|
|
1088
|
+
|
|
1027
1089
|
export function generateTsClient(modules: readonly DomainModule[], opts: TsClientOptions): string {
|
|
1028
1090
|
const topologyModules = modules;
|
|
1029
1091
|
// A public business birth can target a deliberately hidden root aggregate. Its private binder still
|
|
@@ -1136,6 +1198,27 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1136
1198
|
),
|
|
1137
1199
|
),
|
|
1138
1200
|
);
|
|
1201
|
+
const hasVersionedRefs = modules.some((m) =>
|
|
1202
|
+
m.aggregates.some((a) =>
|
|
1203
|
+
Object.values(a.fields as Record<string, Field>).some((field) => field.versionedForeignRef !== undefined),
|
|
1204
|
+
) || (m.directives as Directive<unknown>[]).some((directive) => {
|
|
1205
|
+
const birth = (directive as { businessBirth?: { publicPayloadSchema?: z.ZodTypeAny } }).businessBirth;
|
|
1206
|
+
const schema = birth?.publicPayloadSchema ?? (directive as { payloadSchema?: z.ZodTypeAny }).payloadSchema;
|
|
1207
|
+
return schema !== undefined && zodKind(schema) === "object" &&
|
|
1208
|
+
Object.values(zodObjectShape(schema)).some((field) => versionedRefOfMetaOf(field) !== undefined);
|
|
1209
|
+
}),
|
|
1210
|
+
);
|
|
1211
|
+
const hasSha256Digests = modules.some((module) =>
|
|
1212
|
+
module.aggregates.some((aggregate) =>
|
|
1213
|
+
Object.values(aggregate.fields as Record<string, Field>).some((field) =>
|
|
1214
|
+
isSha256DigestSchema(field.zod as z.ZodTypeAny),
|
|
1215
|
+
),
|
|
1216
|
+
) || (module.directives as Directive<unknown>[]).some((directive) => {
|
|
1217
|
+
const birth = (directive as { businessBirth?: { publicPayloadSchema?: z.ZodTypeAny } }).businessBirth;
|
|
1218
|
+
const schema = birth?.publicPayloadSchema ?? (directive as { payloadSchema?: z.ZodTypeAny }).payloadSchema;
|
|
1219
|
+
return schema !== undefined && zodUsesSha256Digest(schema);
|
|
1220
|
+
}),
|
|
1221
|
+
);
|
|
1139
1222
|
const hasConflictResolvers = modules.some(
|
|
1140
1223
|
(m) => ((m as { surfacedResolvers?: readonly unknown[] }).surfacedResolvers?.length ?? 0) > 0,
|
|
1141
1224
|
);
|
|
@@ -1182,6 +1265,20 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1182
1265
|
``,
|
|
1183
1266
|
`import { nomosInternalOpenGeneratedApplication, nomosInternalOpenGeneratedChildApplication${hasInvitations ? ", nomosInternalCreateInvitation, nomosInternalRedeemInvitation" : ""} } from "@githolon/client/_generated_application_host_7c1f5e";`,
|
|
1184
1267
|
``,
|
|
1268
|
+
...(hasSha256Digests
|
|
1269
|
+
? [
|
|
1270
|
+
`/** A SHA-256 digest in canonical Nomos wire form: 64 lowercase hexadecimal characters. */`,
|
|
1271
|
+
`export type Sha256Digest = string & { readonly __nomosSha256Digest: unique symbol };`,
|
|
1272
|
+
`/** Validate and construct a Sha256Digest before an offer is assembled. */`,
|
|
1273
|
+
`export const sha256Digest = (value: string): Sha256Digest => {`,
|
|
1274
|
+
` if (!/^[0-9a-f]{64}$/.test(value)) {`,
|
|
1275
|
+
` throw new TypeError("expected a 64-character lowercase hexadecimal SHA-256 digest");`,
|
|
1276
|
+
` }`,
|
|
1277
|
+
` return value as Sha256Digest;`,
|
|
1278
|
+
`};`,
|
|
1279
|
+
``,
|
|
1280
|
+
]
|
|
1281
|
+
: []),
|
|
1185
1282
|
`interface _NomosApplicationStatus {`,
|
|
1186
1283
|
` readonly phase: "opening" | "current" | "saving" | "receiving" | "catching-up" | "offline" | "attention";`,
|
|
1187
1284
|
` readonly localChanges: number;`,
|
|
@@ -1210,7 +1307,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1210
1307
|
]
|
|
1211
1308
|
: []),
|
|
1212
1309
|
``,
|
|
1213
|
-
`/**
|
|
1310
|
+
`/** Content identity of the installed typed USDA closure (transported as \`.law.usdz\`). */`,
|
|
1214
1311
|
`const ${hashConst} = ${JSON.stringify(opts.domainHash)};`,
|
|
1215
1312
|
``,
|
|
1216
1313
|
...(clientInterface !== undefined
|
|
@@ -1248,6 +1345,15 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1248
1345
|
` /** Stable, portable nomos:// address. An address identifies a record; it never grants authority. */`,
|
|
1249
1346
|
` readonly address: string;`,
|
|
1250
1347
|
`}`,
|
|
1348
|
+
`/** An optional first-class relationship expressed as a REQUIRED business choice.`,
|
|
1349
|
+
` * Use NomosOptionalRef.absent() to deliberately omit it or present(ref) to supply it. */`,
|
|
1350
|
+
`export type NomosOptionalRef<TAggregate> =`,
|
|
1351
|
+
` | { readonly kind: "absent" }`,
|
|
1352
|
+
` | { readonly kind: "present"; readonly reference: NomosRef<TAggregate> };`,
|
|
1353
|
+
`export const NomosOptionalRef = {`,
|
|
1354
|
+
` absent: (): { readonly kind: "absent" } => ({ kind: "absent" }),`,
|
|
1355
|
+
` present: <TAggregate>(reference: NomosRef<TAggregate>): NomosOptionalRef<TAggregate> => ({ kind: "present", reference }),`,
|
|
1356
|
+
`} as const;`,
|
|
1251
1357
|
`interface _NomosReferenceData<TAggregate> extends NomosRef<TAggregate> { workspace: string; type: string; id: string; address: string }`,
|
|
1252
1358
|
`const _nmsReferenceData = <TAggregate>(ref: NomosRef<TAggregate>): _NomosReferenceData<TAggregate> => ref as _NomosReferenceData<TAggregate>;`,
|
|
1253
1359
|
`const _nmsRef = <TAggregate>(workspace: string, type: string, id: string): NomosRef<TAggregate> => ({`,
|
|
@@ -1360,18 +1466,23 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1360
1466
|
`/** Finish a business action without leaking ledger heads or framework write envelopes. */`,
|
|
1361
1467
|
`async function _commit(r: Promise<_NomosRawWriteResult>): Promise<void> { void await r; }`,
|
|
1362
1468
|
`/** Turn the sealed intent's complete affected set into its generated, business-named result. */`,
|
|
1363
|
-
`async function _businessResult<TResult>(ws: string, outputs: readonly { member: string; type: string; many?: boolean }[], r: Promise<_NomosRawWriteResult>): Promise<TResult> {`,
|
|
1469
|
+
`async function _businessResult<TResult>(ws: string, outputs: readonly { member: string; type: string; many?: boolean; optional?: boolean }[], r: Promise<_NomosRawWriteResult>): Promise<TResult> {`,
|
|
1364
1470
|
` const result = await r;`,
|
|
1365
1471
|
` if (typeof result === "string" || !Array.isArray(result.affected)) {`,
|
|
1366
1472
|
` throw new Error("Nomos committed the business action without returning its affected records");`,
|
|
1367
1473
|
` }`,
|
|
1368
|
-
` const answer: Record<string, NomosRef<_NomosAnyAggregate> | readonly NomosRef<_NomosAnyAggregate>[]> = {};`,
|
|
1474
|
+
` const answer: Record<string, NomosRef<_NomosAnyAggregate> | readonly NomosRef<_NomosAnyAggregate>[] | undefined> = {};`,
|
|
1369
1475
|
` for (const output of outputs) {`,
|
|
1370
1476
|
` const matches = result.affected.filter((affected) => affected.type === output.type || affected.type.endsWith("." + output.type));`,
|
|
1371
1477
|
` if (output.many) {`,
|
|
1372
1478
|
` answer[output.member] = matches.map((match) => _nmsRef<_NomosAnyAggregate>(ws, match.type, match.id));`,
|
|
1373
1479
|
` continue;`,
|
|
1374
1480
|
` }`,
|
|
1481
|
+
` if (output.optional) {`,
|
|
1482
|
+
` if (matches.length > 1) throw new Error(\`Nomos expected at most one affected \${output.type} record, received \${matches.length}\`);`,
|
|
1483
|
+
` answer[output.member] = matches.length === 0 ? undefined : _nmsRef<_NomosAnyAggregate>(ws, matches[0]!.type, matches[0]!.id);`,
|
|
1484
|
+
` continue;`,
|
|
1485
|
+
` }`,
|
|
1375
1486
|
` if (matches.length !== 1) throw new Error(\`Nomos expected one affected ${"${output.type}"} record, received ${"${matches.length}"}\`);`,
|
|
1376
1487
|
` answer[output.member] = _nmsRef<_NomosAnyAggregate>(ws, matches[0]!.type, matches[0]!.id);`,
|
|
1377
1488
|
` }`,
|
|
@@ -1447,6 +1558,34 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1447
1558
|
` async exists() { return (await this.resolveResult()).status === "found"; },`,
|
|
1448
1559
|
` };`,
|
|
1449
1560
|
`}`,
|
|
1561
|
+
...(hasVersionedRefs
|
|
1562
|
+
? [
|
|
1563
|
+
`export type ReferenceFreshness<TVersion> =`,
|
|
1564
|
+
` | { readonly status: "current"; readonly latest: NomosRef<TVersion> }`,
|
|
1565
|
+
` | { readonly status: "stale"; readonly latest: NomosRef<TVersion> }`,
|
|
1566
|
+
` | { readonly status: "unavailable"; readonly reason: string };`,
|
|
1567
|
+
`/** An immutable publication pin plus the stable series which declares its current publication.`,
|
|
1568
|
+
` * freshness() is O(current state): it never reconstructs the pinned record or walks history. */`,
|
|
1569
|
+
`export interface VersionedNomosRef<TVersion, TSeries> {`,
|
|
1570
|
+
` readonly pinned: NomosRef<TVersion>;`,
|
|
1571
|
+
` readonly series: NomosRef<TSeries>;`,
|
|
1572
|
+
` freshness(): Promise<ReferenceFreshness<TVersion>>;`,
|
|
1573
|
+
`}`,
|
|
1574
|
+
`type _NomosRawAddress = { workspace: string; type: string; id: string };`,
|
|
1575
|
+
`function _versionedNomosRef<TVersion, TSeries>(nomos: NomosHolon, raw: { pinned: _NomosRawAddress; series: _NomosRawAddress }, currentField: string): VersionedNomosRef<TVersion, TSeries> {`,
|
|
1576
|
+
` const pinned = _nmsRef<TVersion>(raw.pinned.workspace, raw.pinned.type, raw.pinned.id);`,
|
|
1577
|
+
` const series = _nmsRef<TSeries>(raw.series.workspace, raw.series.type, raw.series.id);`,
|
|
1578
|
+
` return { pinned, series, async freshness() {`,
|
|
1579
|
+
` if (nomos.checkRefFreshness === undefined) return { status: "unavailable", reason: "installed Nomos runtime does not support reference freshness" };`,
|
|
1580
|
+
` const result = await nomos.checkRefFreshness([{ pinned: raw.pinned, series: raw.series, currentField }]);`,
|
|
1581
|
+
` const outcome = result.unique[0]?.outcome;`,
|
|
1582
|
+
` if (outcome?.status === "current") return { status: "current", latest: pinned };`,
|
|
1583
|
+
` if (outcome?.status === "stale" && outcome.latest !== undefined) return { status: "stale", latest: _nmsRef<TVersion>(outcome.latest.workspace, outcome.latest.type, outcome.latest.id) };`,
|
|
1584
|
+
` return { status: "unavailable", reason: String(outcome?.status ?? "unknown") };`,
|
|
1585
|
+
` } };`,
|
|
1586
|
+
`}`,
|
|
1587
|
+
]
|
|
1588
|
+
: []),
|
|
1450
1589
|
``,
|
|
1451
1590
|
`/** Arbitrary JSON — the type a RECURSIVE \`z.lazy\` payload subtree degrades to (a self-`,
|
|
1452
1591
|
` * referencing tree/JSON schema can't be a finite named literal, so the engine validates it`,
|
|
@@ -1542,12 +1681,12 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1542
1681
|
` /** THE ONE ENCODER offer (the single v2-envelope codec lives in @githolon/client \`encodeIntentOfferBytes\`):`,
|
|
1543
1682
|
` * hand the holon a PLANNED directive + the verified \`actor\`; the holon encodes + offers. The generated`,
|
|
1544
1683
|
` * client calls THIS, never building offer bytes itself — so there is exactly ONE copy of the wire format. */`,
|
|
1545
|
-
` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean
|
|
1684
|
+
` offerDirective(domain: string, directiveId: string, payload: unknown, opts?: { domainHash?: string; actor?: string; returnCreated?: boolean }): Promise<string | { head: string; created?: { type: string; id: string }[]; affected?: { type: string; id: string }[]; workspace?: string; born?: unknown; birthOutcomes?: unknown }>;`,
|
|
1546
1685
|
...(sourceTrustPresent
|
|
1547
1686
|
? [
|
|
1548
|
-
` /** FRAMEWORK-INTERNAL:
|
|
1549
|
-
` *
|
|
1550
|
-
`
|
|
1687
|
+
` /** FRAMEWORK-INTERNAL: inspect a sibling's immutable custody identity before authoring`,
|
|
1688
|
+
` * the ordinary source-enrolment offer. This identity is never accepted as read proof. */`,
|
|
1689
|
+
` __nomosWorkspaceGenesis(from: string): Promise<string>;`,
|
|
1551
1690
|
]
|
|
1552
1691
|
: []),
|
|
1553
1692
|
...(hasCorrections
|
|
@@ -1578,6 +1717,12 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1578
1717
|
` __nomosChildWorkspaces?(): Promise<readonly { workspace: string; status: "active" | "retired" | "reclaimed" | "unknown"; bornAt: string | null }[]>;`,
|
|
1579
1718
|
` /** FRAMEWORK-INTERNAL child navigation captured by generated description links. */`,
|
|
1580
1719
|
` __nomosOpenChild?(workspace: string): Promise<NomosHolon>;`,
|
|
1720
|
+
...(hasVersionedRefs
|
|
1721
|
+
? [
|
|
1722
|
+
` /** Framework-private O(current-state) version check used by generated VersionedNomosRef. */`,
|
|
1723
|
+
` checkRefFreshness?(refs: readonly { pinned: _NomosRawAddress; series: _NomosRawAddress; currentField: string }[]): Promise<{ unique: { outcome: { status: string; latest?: _NomosRawAddress } }[]; order: number[] }>;`,
|
|
1724
|
+
]
|
|
1725
|
+
: []),
|
|
1581
1726
|
` /** FRAMEWORK-INTERNAL exact-history adoption seam. Generated clients expose only`,
|
|
1582
1727
|
` * \`model.adopt()\` and a chained \`decision.accept(reason)\`. */`,
|
|
1583
1728
|
` __nomosAdoptModel?(domain: string, candidateHash: string, opts?: { because?: string }): Promise<__NomosModelAdoptionWire>;`,
|
|
@@ -1617,7 +1762,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1617
1762
|
` /** FRAMEWORK-INTERNAL: prepare the atomic law payload behind share/unshare/transfer. */`,
|
|
1618
1763
|
` __nomosPrepareOwnedRecordAction?(args: { action: "share" | "unshare" | "transfer"; scope: string; record: NomosRef<_NomosAnyAggregate>; peer: string }): Promise<Record<string, unknown>>;`,
|
|
1619
1764
|
` /** FRAMEWORK-INTERNAL: derive the deterministic unborn child's owned key before atomic genesis. */`,
|
|
1620
|
-
` __nomosOwnedBirthScopeKey?(args: { scope: string; namespace: string; key: unknown }): Promise<NomosScopeKey & { workspace: string }>;`,
|
|
1765
|
+
` __nomosOwnedBirthScopeKey?(args: { scope: string; namespace: string; key: unknown; record: { type: string; id: string } }): Promise<NomosScopeKey & { workspace: string }>;`,
|
|
1621
1766
|
...(ownedPresent
|
|
1622
1767
|
? [
|
|
1623
1768
|
` /** FRAMEWORK-INTERNAL: make the opaque request for this session's not-yet-approved device. */`,
|
|
@@ -1655,12 +1800,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1655
1800
|
` * OPTIONAL member: structural binding tolerates a pre-spatial holon (the accessor then throws at call). */`,
|
|
1656
1801
|
` spatialWithin?(spatialId: string, bbox: { minX: number; minY: number; maxX: number; maxY: number } | { minLng: number; minLat: number; maxLng: number; maxLat: number }): Promise<unknown>;`,
|
|
1657
1802
|
` sum(sumId: string, groupKey?: string): Promise<number>;`,
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
` /** THE LOGICAL PAGED ROW SURFACE (sharding §5.3, ruling 2) — present on a ROUTED holon. */`,
|
|
1661
|
-
` pages?(queryId: string, params?: Record<string, unknown>, page?: { pageSize?: number; cursor?: string | null }): Promise<{ rows: unknown[]; cursor: string | null; done: boolean }>;`,
|
|
1662
|
-
]
|
|
1663
|
-
: []),
|
|
1803
|
+
` /** Bounded authenticated query page. Optional only so generated clients diagnose an old runtime cleanly. */`,
|
|
1804
|
+
` queryPage?(queryId: string, params?: Record<string, unknown>, page?: { pageSize?: number; cursor?: string | null }, keys?: readonly unknown[], principal?: string): Promise<{ rows: unknown[]; cursor: string | null; done: boolean }>;`,
|
|
1664
1805
|
` /** The DEAD-LETTER QUEUE — refused-but-legitimate intents the app must handle`,
|
|
1665
1806
|
` * explicitly (inspect → retry after a law fix → or discard). Work is never lost. */`,
|
|
1666
1807
|
` deadLetters(): Promise<{ id?: string; oid: string; source: string; error: string; domain?: string | null; directiveId?: string | null; at: string }[]>;`,
|
|
@@ -1681,6 +1822,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1681
1822
|
` * and the holon computes the LIVE per-member verdict on \`holon.compat\`. A KNOWN-incompatible directive`,
|
|
1682
1823
|
` * then refuses CLIENT-SIDE at write time (LawMismatchError), never as a server 422. */`,
|
|
1683
1824
|
` checkCompat?(compat: { lawHash: string; interfaceHash: string; packageName: string }, requirement?: unknown): NomosCompatVerdict | null;`,
|
|
1825
|
+
` /** Framework-private readiness hint; generated domain bindings keep first-use plan work off the offer path. */`,
|
|
1826
|
+
` prepareDomain?(domain: string, packageHash: string): void;`,
|
|
1684
1827
|
` /** RELEASE COHERENCE: throws a typed KernelCoherenceError at bind if the cloud's kernel lacks a`,
|
|
1685
1828
|
` * capability this law requires (e.g. \`compoundKeys\`) — instead of an opaque deep fold refusal. */`,
|
|
1686
1829
|
` assertKernelCoherence?(requiresKernel: readonly string[], opts?: { lawHash?: string }): unknown;`,
|
|
@@ -2136,7 +2279,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2136
2279
|
`const _nmsWireReferences = <T extends object>(payload: T, fields: Readonly<Record<string, string>>, workspace: string): T => {`,
|
|
2137
2280
|
` const out = { ...payload } as Record<string, unknown>;`,
|
|
2138
2281
|
` for (const [field, expectedType] of Object.entries(fields)) {`,
|
|
2139
|
-
`
|
|
2282
|
+
` let value = out[field];`,
|
|
2283
|
+
` if (typeof value === "object" && value !== null && "kind" in value) {`,
|
|
2284
|
+
` const choice = value as { kind?: unknown; reference?: unknown };`,
|
|
2285
|
+
` if (choice.kind === "absent") { delete out[field]; continue; }`,
|
|
2286
|
+
` if (choice.kind !== "present") throw new TypeError(field + " must choose NomosOptionalRef.absent() or .present(ref)");`,
|
|
2287
|
+
` value = choice.reference;`,
|
|
2288
|
+
` }`,
|
|
2140
2289
|
` if (value !== undefined) out[field] = _nmsReferenceId(value, expectedType, workspace, field);`,
|
|
2141
2290
|
` }`,
|
|
2142
2291
|
` return out as T;`,
|
|
@@ -2144,52 +2293,36 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2144
2293
|
``,
|
|
2145
2294
|
);
|
|
2146
2295
|
}
|
|
2147
|
-
if (
|
|
2296
|
+
if (hasVersionedRefs) {
|
|
2148
2297
|
out.push(
|
|
2149
|
-
`const
|
|
2298
|
+
`const _nmsWireVersionedReferences = <T extends object>(payload: T, fields: readonly string[]): T => {`,
|
|
2150
2299
|
` const out = { ...payload } as Record<string, unknown>;`,
|
|
2151
2300
|
` for (const field of fields) {`,
|
|
2152
|
-
` const value = out[field];`,
|
|
2153
|
-
` if (value
|
|
2301
|
+
` const value = out[field] as { pinned?: unknown; series?: unknown } | undefined;`,
|
|
2302
|
+
` if (value === undefined) continue;`,
|
|
2303
|
+
` if (typeof value !== "object" || value === null) throw new TypeError(field + " must be a VersionedNomosRef returned by the generated application");`,
|
|
2304
|
+
` const address = (part: unknown, label: string): _NomosRawAddress => {`,
|
|
2305
|
+
` if (typeof part !== "object" || part === null) throw new TypeError(field + "." + label + " must carry a NomosRef");`,
|
|
2306
|
+
` const ref = _nmsReferenceData(part as NomosRef<_NomosAnyAggregate>);`,
|
|
2307
|
+
` if (!ref.workspace || !ref.type || !ref.id) throw new TypeError(field + "." + label + " must carry a complete NomosRef");`,
|
|
2308
|
+
` return { workspace: ref.workspace, type: ref.type, id: ref.id };`,
|
|
2309
|
+
` };`,
|
|
2310
|
+
` out[field] = { pinned: address(value.pinned, "pinned"), series: address(value.series, "series") };`,
|
|
2154
2311
|
` }`,
|
|
2155
2312
|
` return out as T;`,
|
|
2156
2313
|
`};`,
|
|
2157
2314
|
``,
|
|
2158
2315
|
);
|
|
2159
2316
|
}
|
|
2160
|
-
|
|
2161
|
-
if (sourceTrustPresent) {
|
|
2317
|
+
if (anyRecipientPayload) {
|
|
2162
2318
|
out.push(
|
|
2163
|
-
|
|
2164
|
-
`
|
|
2165
|
-
`
|
|
2166
|
-
`
|
|
2167
|
-
`
|
|
2168
|
-
` const raw = typeof value === "object" && value !== null && "entry" in value`,
|
|
2169
|
-
` ? (value as { entry?: unknown }).entry`,
|
|
2170
|
-
` : value;`,
|
|
2171
|
-
` if (typeof raw !== "object" || raw === null) throw new Error("Nomos source did not return an attested business record");`,
|
|
2172
|
-
` const entry = raw as { queryId?: unknown; result?: unknown; attestation?: unknown };`,
|
|
2173
|
-
` if (typeof entry.queryId !== "string" || typeof entry.attestation !== "object" || entry.attestation === null) {`,
|
|
2174
|
-
` throw new Error("Nomos source omitted its signed identity");`,
|
|
2319
|
+
`const _nmsWireRecipients = <T extends object>(payload: T, fields: readonly string[]): T => {`,
|
|
2320
|
+
` const out = { ...payload } as Record<string, unknown>;`,
|
|
2321
|
+
` for (const field of fields) {`,
|
|
2322
|
+
` const value = out[field];`,
|
|
2323
|
+
` if (value !== undefined) out[field] = _nmsRecipientAddress(value, field);`,
|
|
2175
2324
|
` }`,
|
|
2176
|
-
` return
|
|
2177
|
-
`};`,
|
|
2178
|
-
`const _nmsSourceGenesis = (entry: _NomosAttestedEntry): string => {`,
|
|
2179
|
-
` const value = entry.attestation.source_genesis;`,
|
|
2180
|
-
` if (typeof value !== "string" || value.length === 0) throw new Error("Nomos source omitted its genesis identity");`,
|
|
2181
|
-
` return value;`,
|
|
2182
|
-
`};`,
|
|
2183
|
-
`const _nmsSourceKeyHash = async (entry: _NomosAttestedEntry): Promise<string> => {`,
|
|
2184
|
-
` const publicKey = entry.attestation.public_key;`,
|
|
2185
|
-
` if (typeof publicKey !== "string" || publicKey.length === 0) throw new Error("Nomos source omitted its attestation key");`,
|
|
2186
|
-
` let bytes: Uint8Array;`,
|
|
2187
|
-
` try {`,
|
|
2188
|
-
` if (typeof atob === "function") bytes = Uint8Array.from(atob(publicKey), (character) => character.charCodeAt(0));`,
|
|
2189
|
-
` else bytes = Uint8Array.from((globalThis as unknown as { Buffer: { from(value: string, encoding: string): Uint8Array } }).Buffer.from(publicKey, "base64"));`,
|
|
2190
|
-
` } catch { throw new Error("Nomos source returned an invalid attestation key"); }`,
|
|
2191
|
-
` const digest = await crypto.subtle.digest("SHA-256", bytes as unknown as BufferSource);`,
|
|
2192
|
-
` return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");`,
|
|
2325
|
+
` return out as T;`,
|
|
2193
2326
|
`};`,
|
|
2194
2327
|
``,
|
|
2195
2328
|
);
|
|
@@ -2261,14 +2394,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2261
2394
|
|
|
2262
2395
|
out.push(`// ── read models (projection \`data\` shapes; partial-fold optional, EXCEPT .fromBirth()`, `// fields — their presence is gate law, so the type is non-optional) ──`, ``);
|
|
2263
2396
|
const foreignRefFields: { agg: string; field: string; workspaceField: string; target: string }[] = [];
|
|
2397
|
+
const atomicRefFields: { agg: string; field: string }[] = [];
|
|
2398
|
+
const versionedRefFields: { agg: string; field: string; currentField: string }[] = [];
|
|
2264
2399
|
// wrap a read accessor return expression through the foreignRef row-hydrator (identity + byte-identical
|
|
2265
2400
|
// when the package declares no foreignRef, so non-foreignRef packages are unchanged).
|
|
2266
2401
|
// Every read accessor's rows get their canonical `.ref` stamped from `holon.workspace` (additive); a
|
|
2267
2402
|
// foreignRef-declaring package ALSO hydrates its foreignRef fields into typed locators first.
|
|
2268
2403
|
const wrapP = (expr: string): string =>
|
|
2269
|
-
`_businessRecordsP(_stampRefsP(holon.workspace, ${foreignRefFields.length > 0 ? `_hydrateP(holon, ${expr})` : expr}))`;
|
|
2404
|
+
`_businessRecordsP(_stampRefsP(holon.workspace, ${foreignRefFields.length + atomicRefFields.length + versionedRefFields.length > 0 ? `_hydrateP(holon, ${expr})` : expr}))`;
|
|
2270
2405
|
const wrapRows = (expr: string): string =>
|
|
2271
|
-
`_businessRecords(_stampRefs(holon.workspace, ${foreignRefFields.length > 0 ? `_hydrateRows(holon, ${expr})` : expr}))`;
|
|
2406
|
+
`_businessRecords(_stampRefs(holon.workspace, ${foreignRefFields.length + atomicRefFields.length + versionedRefFields.length > 0 ? `_hydrateRows(holon, ${expr})` : expr}))`;
|
|
2272
2407
|
// ATTRIBUTED READS (architecture/field_provenance.md): the shared provenance shape +
|
|
2273
2408
|
// per-aggregate accessors — emitted ONLY when the package declares ≥1 `.attributed()`
|
|
2274
2409
|
// field, so a provenance-free package's client is byte-identical (the era rule).
|
|
@@ -2297,6 +2432,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2297
2432
|
out.push(` ${tsKey(name)}${required ? "" : "?"}: ${tsTypeOfField(field as Field)};`);
|
|
2298
2433
|
const fr = (field as { foreignRef?: { workspaceField: string; targetAggregateId: string } }).foreignRef;
|
|
2299
2434
|
if (fr !== undefined) foreignRefFields.push({ agg: typeId, field: name, workspaceField: fr.workspaceField, target: fr.targetAggregateId });
|
|
2435
|
+
if ((field as Field).nomosRef !== undefined) atomicRefFields.push({ agg: typeId, field: name });
|
|
2436
|
+
const vr = (field as Field).versionedForeignRef;
|
|
2437
|
+
if (vr !== undefined) versionedRefFields.push({ agg: typeId, field: name, currentField: vr.currentField });
|
|
2300
2438
|
}
|
|
2301
2439
|
if (attributed.length > 0) {
|
|
2302
2440
|
out.push(
|
|
@@ -2337,7 +2475,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2337
2475
|
// row into a `ForeignRef<TargetAggregate>` — reading the foreign id (the field) + the foreign workspace id (the
|
|
2338
2476
|
// declared sibling field). The read model stays honest (the field is the raw stored id string); this gives
|
|
2339
2477
|
// the TYPED locator with resolve/watch on top, without cloning the other holon.
|
|
2340
|
-
if (foreignRefFields.length > 0) {
|
|
2478
|
+
if (foreignRefFields.length + atomicRefFields.length + versionedRefFields.length > 0) {
|
|
2341
2479
|
const byAgg = new Map<string, typeof foreignRefFields>();
|
|
2342
2480
|
for (const f of foreignRefFields) { const l = byAgg.get(f.agg) ?? []; l.push(f); byAgg.set(f.agg, l); }
|
|
2343
2481
|
out.push(`// ── foreign-ref locators (t.foreignRef): typed cross-workspace edges (resolve/watch on demand) ──`);
|
|
@@ -2360,15 +2498,27 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2360
2498
|
// `row.data.<field>.id` is still the raw id. Identity for any aggregate without foreignRefs.
|
|
2361
2499
|
const hydrateSpecs: Record<string, { field: string; wsField: string; target: string }[]> = {};
|
|
2362
2500
|
for (const f of foreignRefFields) { (hydrateSpecs[f.agg] ??= []).push({ field: f.field, wsField: f.workspaceField, target: f.target }); }
|
|
2501
|
+
const atomicSpecs: Record<string, { field: string }[]> = {};
|
|
2502
|
+
for (const f of atomicRefFields) { (atomicSpecs[f.agg] ??= []).push({ field: f.field }); }
|
|
2503
|
+
const versionedSpecs: Record<string, { field: string; currentField: string }[]> = {};
|
|
2504
|
+
for (const f of versionedRefFields) { (versionedSpecs[f.agg] ??= []).push({ field: f.field, currentField: f.currentField }); }
|
|
2363
2505
|
out.push(
|
|
2364
2506
|
`const _FOREIGN_REFS: Record<string, { field: string; wsField: string; target: string }[]> = ${JSON.stringify(hydrateSpecs)};`,
|
|
2507
|
+
`const _ATOMIC_REFS: Record<string, { field: string }[]> = ${JSON.stringify(atomicSpecs)};`,
|
|
2508
|
+
`const _VERSIONED_REFS: Record<string, { field: string; currentField: string }[]> = ${JSON.stringify(versionedSpecs)};`,
|
|
2365
2509
|
`function _hydrateRows<T>(nomos: NomosForeignReader, rows: Row<T>[]): Row<T>[] {`,
|
|
2366
2510
|
` return rows.map((row) => {`,
|
|
2367
2511
|
` const raw = row as Row<T> & { type: string };`,
|
|
2368
|
-
` const specs = _FOREIGN_REFS[raw.type];`,
|
|
2369
|
-
`
|
|
2512
|
+
` const specs = _FOREIGN_REFS[raw.type] ?? [];`,
|
|
2513
|
+
` const atomic = _ATOMIC_REFS[raw.type] ?? [];`,
|
|
2514
|
+
` const versioned = _VERSIONED_REFS[raw.type] ?? [];`,
|
|
2515
|
+
` if ((specs.length + atomic.length + versioned.length) === 0 || row.data == null) return row;`,
|
|
2370
2516
|
` const data: Record<string, unknown> = { ...(row.data as Record<string, unknown>) };`,
|
|
2371
2517
|
` for (const s of specs) { const v = data[s.field]; if (typeof v === "string") data[s.field] = foreignRef(nomos, s.target, v, String(data[s.wsField] ?? "")); }`,
|
|
2518
|
+
` for (const s of atomic) { const v = data[s.field] as { workspace: string; type: string; id: string } | undefined; if (v && typeof v === "object") data[s.field] = _nmsRef(v.workspace, v.type, v.id); }`,
|
|
2519
|
+
...(hasVersionedRefs
|
|
2520
|
+
? [` for (const s of versioned) { const v = data[s.field] as { pinned: _NomosRawAddress; series: _NomosRawAddress } | undefined; if (v && typeof v === "object") data[s.field] = _versionedNomosRef(nomos as NomosHolon, v, s.currentField); }`]
|
|
2521
|
+
: []),
|
|
2372
2522
|
` return { ...row, data: data as T };`,
|
|
2373
2523
|
` });`,
|
|
2374
2524
|
`}`,
|
|
@@ -2621,6 +2771,44 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2621
2771
|
const runtimeTypeOf = (aggregate: AggregateHandle): string => isNamespacedAggregate(aggregate)
|
|
2622
2772
|
? qualifyReadProjectionType(domain, aggregate.id)
|
|
2623
2773
|
: aggregate.id;
|
|
2774
|
+
const versionedFactories = new Map<string, ReturnType<typeof versionedRefOfMetaOf>>();
|
|
2775
|
+
for (const directive of dirs) {
|
|
2776
|
+
if (!isBusinessIntent(directive)) continue;
|
|
2777
|
+
const schema = (directive as { payloadSchema: z.ZodTypeAny }).payloadSchema;
|
|
2778
|
+
if (zodKind(schema) !== "object") continue;
|
|
2779
|
+
for (const [field, fieldSchema] of Object.entries(zodObjectShape(schema))) {
|
|
2780
|
+
const meta = versionedRefOfMetaOf(fieldSchema as z.ZodTypeAny);
|
|
2781
|
+
if (meta === undefined) continue;
|
|
2782
|
+
const method = `pin${cap(camel(field))}`;
|
|
2783
|
+
const prior = versionedFactories.get(method);
|
|
2784
|
+
if (prior !== undefined && JSON.stringify(prior) !== JSON.stringify(meta)) {
|
|
2785
|
+
throw new Error(`generated version-pin factory '${method}' names incompatible reference contracts`);
|
|
2786
|
+
}
|
|
2787
|
+
versionedFactories.set(method, meta);
|
|
2788
|
+
}
|
|
2789
|
+
}
|
|
2790
|
+
for (const [method, meta] of [...versionedFactories].sort(([a], [b]) => a.localeCompare(b))) {
|
|
2791
|
+
if (meta === undefined) continue;
|
|
2792
|
+
const target = meta.targetKind === "contract"
|
|
2793
|
+
? contractTypeName(meta.targetAggregateType)
|
|
2794
|
+
: meta.targetAggregateType;
|
|
2795
|
+
const series = meta.seriesKind === "contract"
|
|
2796
|
+
? contractTypeName(meta.seriesAggregateType)
|
|
2797
|
+
: meta.seriesAggregateType;
|
|
2798
|
+
const seriesInput = meta.seriesKind === "contract"
|
|
2799
|
+
? meta.seriesAggregateType
|
|
2800
|
+
: series;
|
|
2801
|
+
methods.push(
|
|
2802
|
+
` /** Pin one immutable publication to its stable series. The current pointer is compiled law. */`,
|
|
2803
|
+
` ${method}: (pinned: NomosRef<${target}>, series: NomosRef<${seriesInput}>): VersionedNomosRef<${target}, ${series}> => {`,
|
|
2804
|
+
` const p = _nmsReferenceData(pinned); const s = _nmsReferenceData(series);`,
|
|
2805
|
+
` _nmsReferenceId(pinned, ${JSON.stringify(meta.targetAggregateType)}, "", "pinned");`,
|
|
2806
|
+
` _nmsReferenceId(series, ${JSON.stringify(meta.seriesAggregateType)}, "", "series");`,
|
|
2807
|
+
` if (p.workspace !== s.workspace) throw new TypeError("A version and its stable series must share one workspace");`,
|
|
2808
|
+
` return _versionedNomosRef<${target}, ${series}>(holon, { pinned: { workspace: p.workspace, type: p.type, id: p.id }, series: { workspace: s.workspace, type: s.type, id: s.id } }, ${JSON.stringify(meta.currentField)});`,
|
|
2809
|
+
` },`,
|
|
2810
|
+
);
|
|
2811
|
+
}
|
|
2624
2812
|
// E2E READ INJECTION: the distinct encryption scopes a read of `typeId` must inject keys for (empty
|
|
2625
2813
|
// ⇒ nothing encrypted on that type ⇒ the accessor is emitted plain, unchanged). The `_readKeys` helper
|
|
2626
2814
|
// returns undefined when no keyStore is bound, so an injected accessor still behaves as today offline.
|
|
@@ -2739,13 +2927,21 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2739
2927
|
.filter(([name, field]) => name !== mint?.field && recipientMetaOf(field) !== undefined)
|
|
2740
2928
|
.map(([name]) => name)
|
|
2741
2929
|
: [];
|
|
2930
|
+
const versionedReferencePayloadFields = zodKind(schema) === "object"
|
|
2931
|
+
? Object.entries(zodObjectShape(schema))
|
|
2932
|
+
.filter(([name, field]) => name !== mint?.field && versionedRefOfMetaOf(field) !== undefined)
|
|
2933
|
+
.map(([name]) => name)
|
|
2934
|
+
: [];
|
|
2742
2935
|
const wireReferences = (expr: string): string => {
|
|
2743
2936
|
const references = referencePayloadFields.length === 0
|
|
2744
2937
|
? expr
|
|
2745
2938
|
: `_nmsWireReferences(${expr}, ${JSON.stringify(referencePayloadTypes)}, holon.workspace)`;
|
|
2746
|
-
|
|
2939
|
+
const versioned = versionedReferencePayloadFields.length === 0
|
|
2747
2940
|
? references
|
|
2748
|
-
: `
|
|
2941
|
+
: `_nmsWireVersionedReferences(${references}, ${JSON.stringify(versionedReferencePayloadFields)})`;
|
|
2942
|
+
return recipientPayloadFields.length === 0
|
|
2943
|
+
? versioned
|
|
2944
|
+
: `_nmsWireRecipients(${versioned}, ${JSON.stringify(recipientPayloadFields)})`;
|
|
2749
2945
|
};
|
|
2750
2946
|
const stamps = autoStampFields(schema);
|
|
2751
2947
|
const placed = placementDirs.has(d.id) ? ["shard"] : [];
|
|
@@ -2811,26 +3007,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2811
3007
|
const ownedBirthPrelude = ownedBirthDescriptor === undefined
|
|
2812
3008
|
? ""
|
|
2813
3009
|
: ` if (!holon.__nomosOwnedBirthScopeKey) throw new Error("this holon predates automatic .owned() birth custody — upgrade @githolon/client");\n` +
|
|
2814
|
-
` const _ownedBirthKey = await holon.__nomosOwnedBirthScopeKey({ scope: ${JSON.stringify(ownedBirthDescriptor.scope)}, namespace: ${JSON.stringify(businessBirth!.namespace)}, key: (${inputPayload} as Record<string, unknown>)[${JSON.stringify(businessBirth!.keyField)}] });`;
|
|
3010
|
+
` const _ownedBirthKey = await holon.__nomosOwnedBirthScopeKey({ scope: ${JSON.stringify(ownedBirthDescriptor.scope)}, namespace: ${JSON.stringify(businessBirth!.namespace)}, key: (${inputPayload} as Record<string, unknown>)[${JSON.stringify(businessBirth!.keyField)}], record: { type: ${JSON.stringify(ownedRuntimeType)}, id: ${JSON.stringify(businessBirth!.rootId)} } });`;
|
|
2815
3011
|
const paramSig = `${argumentName}: ${payloadName}`;
|
|
2816
|
-
const
|
|
2817
|
-
|
|
2818
|
-
: Object.entries(sourceTrust.queryArgs)
|
|
2819
|
-
.sort(([a], [b]) => a.localeCompare(b))
|
|
2820
|
-
.map(([argument, field]) => `${JSON.stringify(argument)}: (${argumentName} as Record<string, unknown>)[${JSON.stringify(field)}]`)
|
|
2821
|
-
.join(", ");
|
|
2822
|
-
const sourceQueryReferenceTypes = sourceTrust === undefined
|
|
2823
|
-
? {}
|
|
2824
|
-
: Object.fromEntries(Object.entries(sourceTrust.queryArgs).flatMap(([argument, field]) => {
|
|
2825
|
-
const type = referencePayloadTypes[field];
|
|
2826
|
-
return type === undefined ? [] : [[argument, type]];
|
|
2827
|
-
}));
|
|
3012
|
+
const payloadPath = (root: string, path: string): string =>
|
|
3013
|
+
path.split(".").reduce((expr, part) => `${expr}[${JSON.stringify(part)}]`, `(${root} as { [key: string]: any })`);
|
|
2828
3014
|
const copiedTrustFields = sourceTrust === undefined
|
|
2829
3015
|
? []
|
|
2830
3016
|
: Object.entries(sourceTrust.copy)
|
|
2831
3017
|
.sort(([a], [b]) => a.localeCompare(b))
|
|
2832
3018
|
.map(([target, field]) =>
|
|
2833
|
-
` ${JSON.stringify(target)}:
|
|
3019
|
+
` ${JSON.stringify(target)}: ${payloadPath(inputPayload, field)},`);
|
|
2834
3020
|
const copiedTrustReferenceTypes = sourceTrust === undefined
|
|
2835
3021
|
? {}
|
|
2836
3022
|
: Object.fromEntries(Object.entries(sourceTrust.copy).flatMap(([target, field]) => {
|
|
@@ -2840,25 +3026,23 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2840
3026
|
const sourceTrustPrelude = sourceTrust === undefined
|
|
2841
3027
|
? []
|
|
2842
3028
|
: [
|
|
2843
|
-
` const _sourceWorkspace = String(
|
|
3029
|
+
` const _sourceWorkspace = String(${payloadPath(inputPayload, sourceTrust.fromPayload)} ?? "");`,
|
|
2844
3030
|
` if (!_sourceWorkspace.trim()) throw new Error(${JSON.stringify(`${d.id} needs a source workspace`)});`,
|
|
2845
|
-
` const
|
|
2846
|
-
` const _sourceEntry = _nmsAttestedEntry(await holon.fetchAttestedRead(_sourceWorkspace, ${JSON.stringify(sourceTrust.queryId)}, _sourceQueryArgs));`,
|
|
3031
|
+
` const _sourceGenesis = await holon.__nomosWorkspaceGenesis(_sourceWorkspace);`,
|
|
2847
3032
|
` await _commit(holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(sourceTrust.viaDirectiveId)}, _nmsWireReferences({`,
|
|
2848
3033
|
...copiedTrustFields,
|
|
2849
|
-
` ${JSON.stringify(sourceTrust.genesisField)}:
|
|
2850
|
-
` ${JSON.stringify(sourceTrust.keyHashesField)}: [
|
|
3034
|
+
` ${JSON.stringify(sourceTrust.genesisField)}: _sourceGenesis,`,
|
|
3035
|
+
` ${JSON.stringify(sourceTrust.keyHashesField)}: [${JSON.stringify("0".repeat(64))}],`,
|
|
2851
3036
|
` }, ${JSON.stringify(copiedTrustReferenceTypes)}, _sourceWorkspace), { domainHash, actor, returnCreated: true }) as Promise<_NomosRawWriteResult>);`,
|
|
2852
3037
|
];
|
|
2853
|
-
const offerExtra = sourceTrust === undefined ? "" : ", attestations: [_sourceEntry]";
|
|
2854
3038
|
const sealPrelude = wantSeal
|
|
2855
3039
|
? ` const _sealed = await _sealFields(holon, keyStore, ${encConst}[${JSON.stringify(domain)}].directives[${JSON.stringify(d.id)}] as Record<string, { scope: string; aggSid: string; fieldSid: string; owned?: true }>, ${inputPayload} as Record<string, unknown>, true${ownedBirthDescriptor !== undefined ? ", _ownedBirthKey" : ", undefined"}${ownedRecordPrelude ? ", _ownedRecord" : ""}) as ${payloadName};`
|
|
2856
3040
|
: "";
|
|
2857
3041
|
const pv = wantSeal ? "_sealed" : inputPayload;
|
|
2858
3042
|
// MULTI-OUTPUT: name every aggregate this directive writes (primary marker target + declared
|
|
2859
3043
|
// secondaryOutputs), so the frontend dev sees "writes A + B" at the call site (not one by convention).
|
|
2860
|
-
const dd = d as unknown as { aggregateId: string; marker: string; secondaryOutputs?: readonly { aggregateId: string; marker: string; many?: true }[] };
|
|
2861
|
-
const allOutputs = [{ aggregateId: dd.aggregateId, marker: dd.marker, many:
|
|
3044
|
+
const dd = d as unknown as { aggregateId: string; marker: string; outputCardinality?: "many" | "optional"; secondaryOutputs?: readonly { aggregateId: string; marker: string; many?: true; optional?: true }[] };
|
|
3045
|
+
const allOutputs = [{ aggregateId: dd.aggregateId, marker: dd.marker, many: dd.outputCardinality === "many", optional: dd.outputCardinality === "optional" }, ...(dd.secondaryOutputs ?? []).map((output) => ({ ...output, many: output.many === true, optional: output.optional === true }))]
|
|
2862
3046
|
.filter((output) => publicAggregateIds.has(output.aggregateId))
|
|
2863
3047
|
.filter((output, index, outputs) =>
|
|
2864
3048
|
outputs.findIndex((candidate) => candidate.aggregateId === output.aggregateId) === index);
|
|
@@ -2886,7 +3070,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2886
3070
|
resultMembers.add(field);
|
|
2887
3071
|
return output.many
|
|
2888
3072
|
? ` readonly ${tsKey(field)}: readonly NomosRef<${output.aggregateId}>[];`
|
|
2889
|
-
:
|
|
3073
|
+
: output.optional
|
|
3074
|
+
? ` readonly ${tsKey(field)}: NomosRef<${output.aggregateId}> | undefined;`
|
|
3075
|
+
: ` readonly ${tsKey(field)}: NomosRef<${output.aggregateId}>;`;
|
|
2890
3076
|
});
|
|
2891
3077
|
out.push(
|
|
2892
3078
|
`/** Every business record affected by \`${domain}/${d.id}\`. No ledger or transport receipt leaks through this type. */`,
|
|
@@ -2976,10 +3162,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
2976
3162
|
member: tsResultMemberName(output.aggregateId),
|
|
2977
3163
|
type: output.aggregateId,
|
|
2978
3164
|
...(output.many ? { many: true } : {}),
|
|
3165
|
+
...(output.optional ? { optional: true } : {}),
|
|
2979
3166
|
})));
|
|
2980
3167
|
const mintedCall = hasBusinessResult
|
|
2981
|
-
? `_businessResult<${resultName}>(holon.workspace, ${resultSpec}, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor, returnCreated: true
|
|
2982
|
-
: `_commit(holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor, returnCreated: true
|
|
3168
|
+
? `_businessResult<${resultName}>(holon.workspace, ${resultSpec}, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor, returnCreated: true }) as Promise<_NomosRawWriteResult>)`
|
|
3169
|
+
: `_commit(holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${body}, { domainHash, actor, returnCreated: true }) as Promise<_NomosRawWriteResult>)`;
|
|
2983
3170
|
methods.push(
|
|
2984
3171
|
` /** Author \`${domain}/${d.id}\`: the generated client MINTS \`${mint.field}\` internally`,
|
|
2985
3172
|
` * — ids are not application input — ${hasBusinessResult ? `and returns its typed ${resultName}` : "and keeps framework write details private"}.${gate ? ` GATED: needs \`{ actor }\` (\`${gate.relation}\`).` : ""} */`,
|
|
@@ -3004,10 +3191,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
3004
3191
|
member: tsResultMemberName(output.aggregateId),
|
|
3005
3192
|
type: output.aggregateId,
|
|
3006
3193
|
...(output.many ? { many: true } : {}),
|
|
3194
|
+
...(output.optional ? { optional: true } : {}),
|
|
3007
3195
|
})));
|
|
3008
3196
|
const commitCall = hasBusinessResult
|
|
3009
|
-
? `_businessResult<${resultName}>(holon.workspace, ${resultSpec}, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true
|
|
3010
|
-
: `_commit(holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true
|
|
3197
|
+
? `_businessResult<${resultName}>(holon.workspace, ${resultSpec}, holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true }) as Promise<_NomosRawWriteResult>)`
|
|
3198
|
+
: `_commit(holon.offerDirective(${JSON.stringify(domain)}, ${JSON.stringify(d.id)}, ${offerPayload}, { domainHash, actor, returnCreated: true }) as Promise<_NomosRawWriteResult>)`;
|
|
3011
3199
|
const outcomeDoc =
|
|
3012
3200
|
d.marker === "creates"
|
|
3013
3201
|
? `Author \`${domain}/${d.id}\`: the LAW mints the ${d.aggregateId ?? "aggregate"} identity and returns every affected business reference — no id input and no query-after.`
|
|
@@ -3066,7 +3254,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
3066
3254
|
const qOwned = qScopes.some((scope) => scope.owned === true);
|
|
3067
3255
|
const qScopeJson = JSON.stringify(qScopes);
|
|
3068
3256
|
methods.push(
|
|
3069
|
-
` /**
|
|
3257
|
+
` /** @deprecated Whole-result reads scale with result size. Use ${lcFirst(camel(q.id))}Pages() for bounded UX.${qScopes.length ? " Encrypted fields decrypt automatically when this signed-in application holds their keys." : ""} */`,
|
|
3070
3258
|
qScopes.length
|
|
3071
3259
|
? qOwned
|
|
3072
3260
|
? ` ${lcFirst(camel(q.id))}: async (params: { ${params} }) => ${wrapP(`_readWithOwnedKeys(holon, keyStore, ${qScopeJson}, (keys) => holon.query(${JSON.stringify(q.id)}, ${wireParams}, keys) as Promise<Row<${q.returns}>[]>)`)},`
|
|
@@ -3085,18 +3273,21 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
3085
3273
|
: ` ${lcFirst(camel(q.id))}: async (params: { ${params} }) => ${wrapP(`historic.query(${JSON.stringify(q.id)}, ${wireParams}, await _readKeys(holon, keyStore, ${qScopeJson})) as Promise<Row<${q.returns}>[]>`)},`
|
|
3086
3274
|
: ` ${lcFirst(camel(q.id))}: (params: { ${params} }) => ${wrapP(`historic.query(${JSON.stringify(q.id)}, ${wireParams}) as Promise<Row<${q.returns}>[]>`)},`,
|
|
3087
3275
|
);
|
|
3088
|
-
|
|
3089
|
-
|
|
3090
|
-
|
|
3091
|
-
|
|
3092
|
-
|
|
3093
|
-
|
|
3094
|
-
|
|
3095
|
-
|
|
3096
|
-
|
|
3097
|
-
|
|
3098
|
-
|
|
3099
|
-
}
|
|
3276
|
+
const queryPageCall = qScopes.length
|
|
3277
|
+
? qOwned
|
|
3278
|
+
? `_readWithOwnedKeys(holon, keyStore, ${qScopeJson}, (keys) => (holon.queryPage ?? (() => { throw new Error("bounded reads need a current @githolon/client"); }))(${JSON.stringify(q.id)}, ${wireParams}, page, keys) as Promise<{ rows: Row<${q.returns}>[]; cursor: string | null; done: boolean }>)`
|
|
3279
|
+
: `(holon.queryPage ?? (() => { throw new Error("bounded reads need a current @githolon/client"); }))(${JSON.stringify(q.id)}, ${wireParams}, page, await _readKeys(holon, keyStore, ${qScopeJson})) as Promise<{ rows: Row<${q.returns}>[]; cursor: string | null; done: boolean }>`
|
|
3280
|
+
: `(holon.queryPage ?? (() => { throw new Error("bounded reads need a current @githolon/client"); }))(${JSON.stringify(q.id)}, ${wireParams}, page) as Promise<{ rows: Row<${q.returns}>[]; cursor: string | null; done: boolean }>`;
|
|
3281
|
+
methods.push(
|
|
3282
|
+
` /** Bounded \`${q.id}\` read. This is the default estate-scale surface: stable opaque cursor, no whole-result hydration. */`,
|
|
3283
|
+
` ${lcFirst(camel(q.id))}Pages: async (params: { ${params} }, page: { pageSize?: number; cursor?: string | null } = {}) => {`,
|
|
3284
|
+
// Parenthesize the asserted Promise before awaiting it. Without these parens TypeScript
|
|
3285
|
+
// parses `await call as Promise<Page>` as `(await call) as Promise<Page>`, leaving `result`
|
|
3286
|
+
// incorrectly typed as a Promise and making every generated `.rows` access invalid.
|
|
3287
|
+
` const result = await (${queryPageCall});`,
|
|
3288
|
+
` return { ...result, rows: ${wrapRows("result.rows")} };`,
|
|
3289
|
+
` },`,
|
|
3290
|
+
);
|
|
3100
3291
|
}
|
|
3101
3292
|
|
|
3102
3293
|
for (const c of (mod.counts ?? []).map((raw) => finishCount(raw))) {
|
|
@@ -3482,6 +3673,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
3482
3673
|
`function ${tsClientFactoryName(domain)}(holon: NomosHolon) {`,
|
|
3483
3674
|
` const domainHash = ${hashConst};`,
|
|
3484
3675
|
` const actor = holon.principal;`,
|
|
3676
|
+
` holon.prepareDomain?.(${JSON.stringify(domain)}, domainHash);`,
|
|
3485
3677
|
...(encPresent ? [` const keyStore: NomosKeyStore | undefined = ${domScopes.length > 0 ? `{ scopeKey: async (scope) => { if (!holon.__nomosScopeKey) throw new Error("encrypted business data needs the current @githolon/client"); return holon.__nomosScopeKey({ scope }); }, readKeys: async (scopes) => { if (!holon.__nomosScopeKeys) throw new Error("encrypted business data needs the current @githolon/client"); return holon.__nomosScopeKeys({ scopes }); } }` : "undefined"};`] : []),
|
|
3486
3678
|
...(ownedScope !== undefined && Object.keys(ownedActions).length > 0
|
|
3487
3679
|
? [
|
|
@@ -3767,7 +3959,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
3767
3959
|
` * or parallel cache around it. Nomos owns offline custody and synchronization. */`,
|
|
3768
3960
|
`export function ${applicationOpener}(options: ${applicationName}OpenOptions): Promise<${applicationName}> {`,
|
|
3769
3961
|
` return nomosInternalOpenGeneratedApplication(`,
|
|
3770
|
-
` { cloud: options.cloud, application: options.application, signIn: options.signIn },`,
|
|
3962
|
+
` { cloud: options.cloud, application: options.application, signIn: options.signIn, lawHash: ${hashConst} },`,
|
|
3771
3963
|
` (holon, user) => _nomosBind${applicationName}(holon as NomosHolon, user),`,
|
|
3772
3964
|
` );`,
|
|
3773
3965
|
`}`,
|