@githolon/dsl 0.102.5 → 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 +73 -33
  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 +1858 -590
  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 +29 -69
  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 +58 -29
  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 +140 -6
  110. package/src/stable_ids.ts +1 -1
  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
package/src/wire.ts CHANGED
@@ -36,9 +36,28 @@ export interface WireHlc {
36
36
  * unit-variant bare string `"Git" | "External"`. */
37
37
  export type WireValue =
38
38
  | { Str: string }
39
+ | { NomosRef: { workspace: string; type: string; id: string } }
40
+ | {
41
+ VersionedNomosRef: {
42
+ pinned: { workspace: string; type: string; id: string };
43
+ series: { workspace: string; type: string; id: string };
44
+ };
45
+ }
39
46
  | { Int: number }
40
47
  | { Set: string[] }
41
48
  | { Map: Record<string, WireField> }
49
+ | {
50
+ RelationshipSubject: {
51
+ status: "active" | "revoked";
52
+ grantedBy: string;
53
+ grantedAt: string;
54
+ revokedBy?: string;
55
+ revokedAt?: string;
56
+ validFromEpoch?: number;
57
+ verifiedVia?: string;
58
+ ownedCustody?: string;
59
+ };
60
+ }
42
61
  | { Evidence: WireEvidenceRef }
43
62
  // First-class canonical JSON (RFC 8785 / JCS) — the committed form of a `t.json()` leaf. The
44
63
  // value rides INLINE (`{Json: <any json>}`), not as a string-of-JSON; the kernel canonicalises
@@ -114,6 +114,17 @@ function describeShape(value: unknown): string {
114
114
  * `op` is passed purely for a NAMED diagnostic (aggregate + field + expected VO type) when a value
115
115
  * can't be lowered — never `field '?'`. */
116
116
  function encodeKernelValue(field: Field | undefined, value: unknown, op?: FieldOp): WireValue {
117
+ if (op?.kernelValue !== undefined) {
118
+ if (
119
+ "RelationshipSubject" in op.kernelValue &&
120
+ (op.aggregateType !== "RelationTuple" || op.field !== "subjects" || op.kind !== "setEntry")
121
+ ) {
122
+ throw new Error(
123
+ "RelationshipSubject is a framework kernel value restricted to RelationTuple.subjects setEntry operations",
124
+ );
125
+ }
126
+ return op.kernelValue;
127
+ }
117
128
  if (value === undefined) {
118
129
  const aggName = op?.aggregateType ?? op?.aggregateId ?? "?";
119
130
  const fieldName = op?.field ?? field?.kind ?? "?";
@@ -141,6 +152,17 @@ function encodeKernelValue(field: Field | undefined, value: unknown, op?: FieldO
141
152
  `t.json({ null: "explicit-empty", reason }) for a real empty value.`,
142
153
  );
143
154
  }
155
+ if (field?.nomosRef !== undefined) {
156
+ const parsed = field.zod.parse(value) as { workspace: string; type: string; id: string };
157
+ return { NomosRef: parsed };
158
+ }
159
+ if (field?.versionedForeignRef !== undefined) {
160
+ const parsed = field.zod.parse(value) as {
161
+ pinned: { workspace: string; type: string; id: string };
162
+ series: { workspace: string; type: string; id: string };
163
+ };
164
+ return { VersionedNomosRef: parsed };
165
+ }
144
166
  if (field?.kind === "evidence") {
145
167
  const v = value as EvidenceRefValue;
146
168
  return {
@@ -79,7 +79,15 @@ import { query } from "./query.js";
79
79
  import { read } from "./read.js";
80
80
  import { sum } from "./sum.js";
81
81
  import { min, max } from "./extremum.js";
82
- import { refAs, workspaceInvariant } from "./framework/workspace_invariant.js";
82
+ import {
83
+ idItem,
84
+ idPayload,
85
+ idTemplate,
86
+ readRef,
87
+ readSet,
88
+ refAs,
89
+ workspaceInvariant,
90
+ } from "./framework/workspace_invariant.js";
83
91
 
84
92
  /** One packed axis the factory derives a placement lane for. */
85
93
  export interface ShardingAxisSpec {
@@ -1152,16 +1160,20 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
1152
1160
  // (`summary-mismatch:…`): held + carried events ⇒ claimed, byte-compared.
1153
1161
  mod["nomosSummaryGate_nomosPropagateSummary"] = workspaceInvariant("nomosSummaryGate:nomosPropagateSummary")
1154
1162
  .on("nomosPropagateSummary")
1155
- .reads(({ intent }) => {
1156
- const shard = String(intent["shard"] ?? "");
1157
- const subtotals = Array.isArray(intent["subtotals"]) ? (intent["subtotals"] as { readId?: unknown; group?: unknown }[]) : [];
1158
- return [
1159
- refAs("frontier", "NomosSummaryFrontier", `pre:${summaryFrontierRowId(shard)}`),
1160
- ...subtotals.map((s, i) =>
1161
- refAs(`prev${i}`, "NomosSummarySubtotal", `pre:${summarySubtotalRowId(shard, String(s.readId ?? ""), String(s.group ?? ""))}`),
1162
- ),
1163
- ];
1164
- })
1163
+ .reads(readSet(
1164
+ readRef("frontier", NomosSummaryFrontier).pre().fromTemplate(idTemplate(
1165
+ "nomos-summary-frontier:",
1166
+ idPayload("shard"),
1167
+ )),
1168
+ readRef("prev", NomosSummarySubtotal).pre().forEach("subtotals", idTemplate(
1169
+ "nomos-subtotal:",
1170
+ idPayload("shard"),
1171
+ "|",
1172
+ idItem("readId", "row-key"),
1173
+ "|",
1174
+ idItem("group", "row-key"),
1175
+ )),
1176
+ ))
1165
1177
  .assert((snapshots, ctx) => {
1166
1178
  const intent = ctx?.intent ?? {};
1167
1179
  const held = snapshots["frontier"]?.["frontier"];
@@ -1212,17 +1224,17 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
1212
1224
  // no skipped or replayed sequence points. ──
1213
1225
  mod["nomosCheckpointGate_nomosCheckpointSeal"] = workspaceInvariant("nomosCheckpointGate:nomosCheckpointSeal")
1214
1226
  .on("nomosCheckpointSeal")
1215
- .reads(({ intent }) => {
1216
- const frontiers = Array.isArray(intent["frontiers"]) ? (intent["frontiers"] as { shard?: unknown }[]) : [];
1217
- const seq = typeof intent["seq"] === "number" ? (intent["seq"] as number) : "?";
1218
- return [
1219
- refAs("head", "NomosCheckpointSeal", `pre:${NOMOS_CHECKPOINT_HEAD_ID}`),
1220
- refAs("own", "NomosCheckpointSeal", `pre:${checkpointSealRowId(seq)}`),
1221
- ...frontiers.map((f, i) =>
1222
- refAs(`front${i}`, "NomosSummaryFrontier", `pre:${summaryFrontierRowId(String(f?.shard ?? ""))}`),
1223
- ),
1224
- ];
1225
- })
1227
+ .reads(readSet(
1228
+ readRef("head", NomosCheckpointSeal).pre().at(NOMOS_CHECKPOINT_HEAD_ID),
1229
+ readRef("own", NomosCheckpointSeal).pre().fromTemplate(idTemplate(
1230
+ "nomos-checkpoint-seal:",
1231
+ idPayload("seq"),
1232
+ )),
1233
+ readRef("front", NomosSummaryFrontier).pre().forEach("frontiers", idTemplate(
1234
+ "nomos-summary-frontier:",
1235
+ idItem("shard"),
1236
+ )),
1237
+ ))
1226
1238
  .assert((snapshots, ctx) => {
1227
1239
  const intent = ctx?.intent ?? {};
1228
1240
  const claimedSeq = typeof intent["seq"] === "number" ? (intent["seq"] as number) : NaN;
@@ -1259,12 +1271,12 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
1259
1271
  // homes it actually holds on the named source; a handoff seal may only land on
1260
1272
  // rows mid-move to exactly its target, with the map version bumped exactly once.
1261
1273
  // Every refusal is typed with the held state — the named remedy. ──
1262
- const moveReads = ({ intent }: { intent: Record<string, unknown> }) => {
1263
- const homeKeys = Array.isArray(intent["homeKeys"]) ? (intent["homeKeys"] as unknown[]) : [];
1264
- return homeKeys.map((k, i) =>
1265
- refAs(`held${i}`, "NomosShardAssignment", `pre:${shardAssignmentRowId(String(k ?? ""))}`),
1266
- );
1267
- };
1274
+ const moveReads = readSet(
1275
+ readRef("held", NomosShardAssignment).pre().forEach("homeKeys", idTemplate(
1276
+ "nomos-shard-assignment:",
1277
+ idItem([], "row-key"),
1278
+ )),
1279
+ );
1268
1280
  mod["nomosMoveGate_nomosSplitShard"] = workspaceInvariant("nomosMoveGate:nomosSplitShard")
1269
1281
  .on("nomosSplitShard")
1270
1282
  .reads(moveReads)
@@ -1326,10 +1338,13 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
1326
1338
  const keyField = route.key;
1327
1339
  mod[`nomosWrongHome_${route.directive}`] = workspaceInvariant(`nomosWrongHome:${route.directive}`)
1328
1340
  .on(route.directive)
1329
- .reads(({ intent }) => [
1330
- refAs("shardIdentity", "NomosShardIdentity", NOMOS_SHARD_IDENTITY_ID),
1331
- refAs("receipt", "NomosHomeReceipt", homeReceiptRowId(String(intent[keyField] ?? ""))),
1332
- ])
1341
+ .reads(readSet(
1342
+ readRef("shardIdentity", NomosShardIdentity).at(NOMOS_SHARD_IDENTITY_ID),
1343
+ readRef("receipt", NomosHomeReceipt).fromTemplate(idTemplate(
1344
+ "nomos-home-receipt:",
1345
+ idPayload(keyField, "row-key"),
1346
+ )),
1347
+ ))
1333
1348
  .assert((snapshots, ctx) => {
1334
1349
  const identity = snapshots["shardIdentity"] ?? {};
1335
1350
  const label = identity["label"];
@@ -1359,7 +1374,7 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
1359
1374
  // ── THE RECEIPT'S OWN HOMING (a receipt claiming a foreign shard never folds) ──
1360
1375
  mod["nomosReceiptHome_nomosReceiveAssignment"] = workspaceInvariant("nomosReceiptHome:nomosReceiveAssignment")
1361
1376
  .on("nomosReceiveAssignment")
1362
- .reads(() => [refAs("shardIdentity", "NomosShardIdentity", NOMOS_SHARD_IDENTITY_ID)])
1377
+ .reads(readSet(readRef("shardIdentity", NomosShardIdentity).at(NOMOS_SHARD_IDENTITY_ID)))
1363
1378
  .assert((snapshots, ctx) => {
1364
1379
  const label = snapshots["shardIdentity"]?.["label"];
1365
1380
  if (typeof label !== "string" || label.length === 0) return { accept: true };
@@ -1437,9 +1452,10 @@ export function shardingLawModule(spec: ShardingLawSpec): Record<string, unknown
1437
1452
  // row WITHOUT authoring).
1438
1453
  mod[`nomosPlacementUnique_${dirId}`] = workspaceInvariant(`nomosPlacementUnique:${dirId}`)
1439
1454
  .on(dirId)
1440
- .reads(({ intent }) => [
1441
- refAs("held", "NomosShardAssignment", `pre:${shardAssignmentRowId(String(intent[keyField] ?? ""))}`),
1442
- ])
1455
+ .reads(readSet(readRef("held", NomosShardAssignment).pre().fromTemplate(idTemplate(
1456
+ "nomos-shard-assignment:",
1457
+ idPayload(keyField, "row-key"),
1458
+ ))))
1443
1459
  .assert((snapshots) => {
1444
1460
  const held = snapshots["held"] ?? {};
1445
1461
  const heldShard = held["shard"];
@@ -16,6 +16,8 @@ import { Lww } from "./drivers.js";
16
16
  import { endorsementOf } from "./principal_evidence.js";
17
17
  import { RelationTuple, writeTuple } from "./framework/relations.js";
18
18
  import {
19
+ readRef,
20
+ readSet,
19
21
  refAs,
20
22
  workspaceInvariant,
21
23
  type WorkspaceInvariantDecl,
@@ -58,6 +60,28 @@ export interface WorkspaceSharingInvitationLaw {
58
60
  readonly pendingByRecipient: QueryDecl;
59
61
  }
60
62
 
63
+ /**
64
+ * Fold the generated invitation law into the same export bag consumed by both
65
+ * package composition and the executable engine registry. Keeping one seam is
66
+ * essential: if the compiler alone injects the redeem directive, custody can
67
+ * advertise a route which the selected runtime plan can never register.
68
+ */
69
+ export function withWorkspaceSharingInvitationExports(
70
+ moduleExports: Record<string, unknown>,
71
+ policies: readonly WorkspaceSharingPolicy[],
72
+ ): Record<string, unknown> {
73
+ if (policies.length === 0) return moduleExports;
74
+ const out = { ...moduleExports };
75
+ for (const policy of policies) {
76
+ const law = policy.invitationLaw;
77
+ out[`__nomosWorkspaceSharingAggregate:${law.claim.id}`] = law.claim;
78
+ out[`__nomosWorkspaceSharingDirective:${law.redeem.id}`] = law.redeem;
79
+ out[`__nomosWorkspaceSharingInvariant:${law.gate.id}`] = law.gate;
80
+ out[`__nomosWorkspaceSharingQuery:${law.pendingByRecipient.id}`] = law.pendingByRecipient;
81
+ }
82
+ return out;
83
+ }
84
+
61
85
  /** One workspace-wide consumption record makes a bearer invitation single-use across every policy. */
62
86
  const NomosInvitationClaim = {
63
87
  ...aggregate("NomosInternalInvitationClaim7c1f5e", {
@@ -204,10 +228,10 @@ export function workspaceSharing(options: WorkspaceSharingOptions): WorkspaceSha
204
228
  });
205
229
  const gate = workspaceInvariant(`NomosInvitationGate:${options.orders.id}`)
206
230
  .on(redeemId)
207
- .reads(({ intent }) => [
208
- refAs("order", options.orders.id, `pre:${String(intent.orderId ?? "")}`),
209
- refAs("claim", NomosInvitationClaim.id, `pre:${String(intent.invitationId ?? "")}`),
210
- ])
231
+ .reads(readSet(
232
+ readRef("order", options.orders).pre().fromPayload("orderId"),
233
+ readRef("claim", NomosInvitationClaim).pre().fromPayload("invitationId"),
234
+ ))
211
235
  .assert((snapshots, ctx) => {
212
236
  const intent = ctx?.intent ?? {};
213
237
  const order = snapshots.order ?? {};
@@ -63,7 +63,7 @@ const SUBJECT_SAMPLES: readonly string[] = [
63
63
  "0".repeat(64),
64
64
  ];
65
65
  const STRING_SAMPLES: readonly string[] = [
66
- "sample", "{}", "en", "en-GB", "AA", "A1", "a", "1", "2026-01-01",
66
+ "sample", "{}", "en", "en-GB", "AA", "A1", "a", "1", "2026-01", "2026-01-01",
67
67
  "000001", "2026-01-01T00:00:00Z", "a@example.com", "https://example.com", "#000000",
68
68
  "00000000-0000-0000-0000-000000000000", "example.com", "0".repeat(40), "0".repeat(64),
69
69
  `nomos-invitation-${"0".repeat(64)}`,
@@ -1,201 +0,0 @@
1
- // THE CLUSTER / ATTESTED-READ ACCEPTANCE — DART LEG (co2's test-cluster ask). Prove the exact
2
- // dynamic-source attested-read scenario (architecture/dynamic_source_attested_reads.md) end-to-end from
3
- // Dart, ONE engine plane, TWO colocated workspaces, ZERO network:
4
- //
5
- // • NomosTestHarness.cluster boots ONE runner holding a `catalogue` session (the source) and an `estate`
6
- // session (the reader), each with its own law installed offline.
7
- // • the catalogue session seeds a listing; the estate session serves + SIGNS it via the in-process
8
- // `attestedRead` op (the colocated source lane) and reads the source's GENESIS off the envelope.
9
- // • NEGATIVE arm: offering `importListing` with that envelope BEFORE enrolling the catalogue refuses
10
- // `AttestedReadUntrustedSource` — surfaced to Dart as a NomosBridgeError carrying that name.
11
- // • HAPPY arm: enrol the source genesis + attestor key, offer `importListing` carrying the signed
12
- // envelope, and read the imported row back through the generated EstateApplication bound to
13
- // `harness.session('estate')`.
14
- @TestOn('vm')
15
- library;
16
-
17
- import 'dart:convert';
18
- import 'dart:io';
19
-
20
- import 'package:crypto/crypto.dart';
21
- import 'package:nomos_client/src/nms_conformance_runtime.dart';
22
- import 'package:nomos_client/src/nms_generated_runtime.dart';
23
- import 'package:test/test.dart';
24
-
25
- // Framework acceptance tests may bind a generated application to an already-mounted harness session. The
26
- // raw client is a private `part` and is deliberately not importable, even here.
27
- import '../../../bench/scale/fixtures/catalogue-reader/build/dart/src/generated/estate.application.dart';
28
-
29
- String get _root => '${Directory.current.path}/../..';
30
- String get _repoEntry => '$_root/cloud/web-client/runner/node-entry.ts';
31
- String get _catDeploy =>
32
- '$_root/bench/scale/fixtures/catalogue-src/build/cat.deploy.json';
33
- String get _estateDeploy =>
34
- '$_root/bench/scale/fixtures/catalogue-reader/build/estate.deploy.json';
35
- String get _estateClientDart =>
36
- '$_root/bench/scale/fixtures/catalogue-reader/build/dart/src/generated/estate.application.dart';
37
- String get _runtimeDir =>
38
- Platform.environment['NOMOS_DART_RUNTIME'] ??
39
- Platform.environment['NOMOS_OFFLINE_RUNTIME'] ??
40
- '${Platform.environment['HOME'] ?? ''}/.holon/runtime';
41
-
42
- String? _missingPrereq() {
43
- if (!File(_repoEntry).existsSync())
44
- return 'in-repo runner not found at $_repoEntry';
45
- for (final p in [_catDeploy, _estateDeploy, _estateClientDart]) {
46
- if (!File(p).existsSync()) {
47
- return 'compile the catalogue fixtures with dart output: '
48
- 'cd bench/scale/fixtures/<catalogue-src|catalogue-reader> && node ../../../../dsl/compile_package.mjs '
49
- '(needs `dart: { support: "package" }` in nomos.package.mjs) — missing $p';
50
- }
51
- }
52
- if (!File('$_runtimeDir/holon-testmint.wasm').existsSync() ||
53
- !File('$_runtimeDir/packages.json').existsSync()) {
54
- return 'warm ~/.holon/runtime first (run any `githolon` command once)';
55
- }
56
- return null;
57
- }
58
-
59
- /// sha256hex of the attestor's raw public bytes — the `keyHash` the reader enrolls (parity with the
60
- /// smoke's `sha256hexBytes(base64decode(public))`).
61
- String _keyHashOf(String base64Public) =>
62
- sha256.convert(base64Decode(base64Public)).toString();
63
-
64
- void main() {
65
- test(
66
- 'cluster attested reads: colocated source served + signed → enrolled → typed import read back',
67
- () async {
68
- final skip = _missingPrereq();
69
- if (skip != null) {
70
- markTestSkipped(skip);
71
- return;
72
- }
73
-
74
- final harness = await NomosTestHarness.cluster(
75
- workspaces: {
76
- 'catalogue': [_catDeploy],
77
- 'estate': [_estateDeploy],
78
- },
79
- // The catalogue SOURCE signs its attested reads with its device key — the colocation resolver uses it,
80
- // and the reader enrols sha256(that public key). (The reader needs no device key: it authors as an actor.)
81
- deviceKeyed: {'catalogue'},
82
- nodeEntry: _repoEntry,
83
- runtimeDir: _runtimeDir,
84
- );
85
- try {
86
- expect(harness.sessions.toSet(), {'catalogue', 'estate'});
87
-
88
- // THE PRODUCTION MULTI-WORKSPACE SURFACE: one bridge = this device's local plane; `bridge.session(ws)`
89
- // is the first-class handle. Enumerate the plane — both workspaces are LOCAL-ONLY (no cloud posture).
90
- final plane = await harness.bridge.listWorkspaces();
91
- expect(plane.map((w) => w['workspace']).toSet(), {'catalogue', 'estate'});
92
- expect(plane.every((w) => w['syncPosture'] == 'local-only'), isTrue,
93
- reason: 'the cluster opens local githolons, not cloud connections');
94
-
95
- // Get the session handles through the production entry (the harness delegates to bridge.session).
96
- final catalogue = harness.bridge.session('catalogue');
97
- final estate = harness.bridge.session('estate');
98
- expect(identical(estate, harness.session('estate')), isTrue,
99
- reason: 'session handles are stable + cached on the plane');
100
-
101
- // 1. seed a listing in the CATALOGUE source (raw offer — the source's own law).
102
- final seeded = await catalogue.offerCreates(
103
- domain: 'cat',
104
- directiveId: 'listCatalogue',
105
- payload: {
106
- 'listingId': 'L-100',
107
- 'title': 'Oak dresser',
108
- 'priceGbp': '420',
109
- 'listedAt': '2026-07-03T00:00:00Z',
110
- },
111
- actor: 'user:seller',
112
- );
113
- expect(seeded.head, isNotEmpty);
114
-
115
- // 2. Read the source's GENESIS (its true identity) + its signing key off ONE attested read. No attestor
116
- // secret: the source session signs with its DEVICE key (its connect authorSecret), which the
117
- // colocation resolver also uses — so the enrolled keyHash matches what the zero-injection lane signs.
118
- final served = await estate.attestedRead(
119
- from: 'catalogue',
120
- queryId: 'listingById',
121
- args: {'listingId': 'L-100'},
122
- );
123
- final entry = (served['entry'] as Map).cast<String, Object?>();
124
- final attestation = (entry['attestation'] as Map).cast<String, Object?>();
125
- final sourceGenesis = (attestation['source_genesis'] ?? '').toString();
126
- final signingKey = (attestation['public_key'] ?? '').toString();
127
- final keyHash = _keyHashOf(signingKey);
128
- expect(sourceGenesis.length, greaterThanOrEqualTo(40),
129
- reason: 'the source genesis oid resolves off the signed envelope');
130
- expect(signingKey, harness.devicePublicKey('catalogue'),
131
- reason:
132
- 'the read is signed by the source session device key the resolver will use');
133
-
134
- // The one importListing offer, offered TWICE (unenrolled then enrolled). ZERO injection: the payload
135
- // names `catalogueWs`, and the runner's colocation resolver serves+signs the foreign read IN-PROCESS —
136
- // NO `attestations` param. The gate judges the served envelope's genesis against the reader's allow-list.
137
- Future<NomosOfferOutcome> importRawForFrameworkTest(String importedAt) =>
138
- estate.offerCreates(
139
- domain: 'estate',
140
- directiveId: 'importListing',
141
- payload: {
142
- 'listingId': 'L-100',
143
- 'catalogueWs': 'catalogue',
144
- 'importedAt': importedAt,
145
- },
146
- actor: 'user:estate-owner',
147
- );
148
-
149
- // 3. NEGATIVE — BEFORE enrolment the source genesis is not in the allow-list → UntrustedSource
150
- // (the resolver still serves+signs; the gate refuses on the untrusted genesis).
151
- Object? negError;
152
- try {
153
- // Deliberately raw: this framework test needs the refused kernel outcome itself. This is framework
154
- // test instrumentation, not public API; tenant code gets only the generated application.
155
- await importRawForFrameworkTest('2026-07-03T12:00:00Z');
156
- } catch (e) {
157
- negError = e;
158
- }
159
- expect(negError, isA<NomosBridgeError>(),
160
- reason: 'an unenrolled source must refuse the import');
161
- expect(negError.toString(), contains('AttestedReadUntrustedSource'),
162
- reason: 'the typed refusal name must surface to Dart');
163
-
164
- // 4. ENROL the catalogue: its genesis + the source device keyHash become folded law-state in the
165
- // reader's own chain (typed write — the reader's own directive, no attestation).
166
- final app =
167
- await nomosInternalBindEstateApplication(estate, 'user:estate-owner');
168
- final enrolled =
169
- await app.trustedCatalogues.enrollCatalogue(EnrollCataloguePayload(
170
- sourceGenesis: sourceGenesis,
171
- keyHashes: [keyHash],
172
- label: 'Acme Catalogue',
173
- enrolledAt: '2026-07-03T10:00:00Z',
174
- ));
175
- expect(NmsInternalNomosRefs.workspace(enrolled.trustedCatalogue), 'estate');
176
-
177
- // 5. HAPPY — the SAME zero-injection offer now admits: the resolver serves+signs, the gate judges the
178
- // genesis against the folded allow-list, and the catalogue title lands on EstateListing.
179
- final imported = await app.estateListings.importListing(
180
- const ImportListingPayload(
181
- listingId: 'L-100',
182
- catalogueWs: 'catalogue',
183
- importedAt: '2026-07-03T12:05:00Z',
184
- ),
185
- );
186
- expect(NmsInternalNomosRefs.workspace(imported.estateListing), 'estate');
187
-
188
- // 6. TYPED read-back through the generated application bound to the estate session.
189
- final row = await app.estateListings.find(imported.estateListing);
190
- expect(row, isNotNull,
191
- reason:
192
- 'the imported EstateListing reads back on the estate session');
193
- expect(row!.title, 'Oak dresser',
194
- reason: 'the imported title was read through the policy edge, typed');
195
- expect(row.catalogue, 'catalogue');
196
- expect(row.listingRef, 'L-100');
197
- } finally {
198
- await harness.dispose();
199
- }
200
- }, timeout: const Timeout(Duration(minutes: 3)));
201
- }