@githolon/dsl 0.100.6 → 0.101.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/dart/CHANGELOG.md +6 -0
  2. package/dart/lib/src/dev_cloud.dart +46 -15
  3. package/dart/lib/src/test_harness.dart +78 -16
  4. package/dart/lib/src/webview_bridge.dart +640 -126
  5. package/dart/pubspec.lock +1 -1
  6. package/dart/pubspec.yaml +2 -2
  7. package/dart/test/bundle_version_pairing_test.dart +11 -22
  8. package/dart/test/co2_product_gauntlet_test.dart +228 -0
  9. package/dart/test/errors_test.dart +63 -12
  10. package/dart/test/first_app_gauntlet_test.dart +29 -3
  11. package/dart/test/webview_bridge_test.dart +274 -0
  12. package/dart_types/CHANGELOG.md +6 -0
  13. package/dart_types/lib/src/errors.dart +136 -30
  14. package/dart_types/lib/src/surfaced.dart +48 -2
  15. package/dart_types/lib/src/wire.dart +3 -1
  16. package/dart_types/pubspec.yaml +1 -1
  17. package/dart_types/test/surfaced_test.dart +19 -0
  18. package/package.json +1 -1
  19. package/src/activity.ts +43 -4
  20. package/src/aggregate.ts +132 -22
  21. package/src/attested_core.ts +1 -1
  22. package/src/authoring.ts +119 -6
  23. package/src/birth_meter.ts +4 -4
  24. package/src/birth_meter_ensure.ts +9 -1
  25. package/src/births.ts +53 -22
  26. package/src/build_package.ts +55 -14
  27. package/src/business_birth.ts +48 -7
  28. package/src/codegen_dart.ts +267 -37
  29. package/src/codegen_dart_client.ts +921 -82
  30. package/src/codegen_openapi.ts +28 -5
  31. package/src/codegen_proof.ts +107 -11
  32. package/src/codegen_ts.ts +961 -135
  33. package/src/compile_package_main.ts +155 -45
  34. package/src/conflicts.ts +252 -0
  35. package/src/describe_model.ts +448 -0
  36. package/src/directive.ts +5 -38
  37. package/src/drivers.ts +12 -4
  38. package/src/engine_entry.ts +4 -0
  39. package/src/evolve_preview.ts +72 -11
  40. package/src/fields.ts +201 -55
  41. package/src/framework/application_home.ts +103 -0
  42. package/src/framework/application_home_domain.ts +2 -0
  43. package/src/framework/attested_receipts.ts +2 -2
  44. package/src/framework/auth_provider_law.ts +51 -24
  45. package/src/framework/business_corrections.ts +170 -0
  46. package/src/framework/disclosure.ts +1 -1
  47. package/src/framework/domain_lifecycle.ts +250 -16
  48. package/src/framework/encryption_identity.ts +1 -1
  49. package/src/framework/encryption_scope.ts +21 -13
  50. package/src/framework/era_certification.ts +1 -1
  51. package/src/framework/framework_aggregates.ts +2 -0
  52. package/src/framework/genesis.ts +1 -1
  53. package/src/framework/home.ts +22 -9
  54. package/src/framework/identity.ts +5 -5
  55. package/src/framework/impure_capability.ts +2 -2
  56. package/src/framework/owned_custody.ts +561 -0
  57. package/src/framework/platform_admin.ts +1 -1
  58. package/src/framework/relations.ts +9 -1
  59. package/src/framework/repair.ts +1 -1
  60. package/src/framework/signing_identity.ts +2 -2
  61. package/src/framework/source_roster.ts +1 -1
  62. package/src/framework/workspaces.ts +17 -10
  63. package/src/generated_client_names.ts +39 -0
  64. package/src/index.ts +20 -15
  65. package/src/kernel_capabilities.ts +25 -1
  66. package/src/law_check.ts +4 -1
  67. package/src/manifest.ts +356 -63
  68. package/src/read_projection_manifest.ts +8 -0
  69. package/src/read_projection_namespace.ts +4 -0
  70. package/src/relation.ts +1 -1
  71. package/src/shape_compat.ts +47 -2
  72. package/src/stable_ids.ts +38 -6
  73. package/src/stable_ids_types.ts +30 -0
  74. package/src/surfaced_lint.ts +0 -0
  75. package/src/usd.ts +62 -11
  76. package/src/usd_layers.ts +54 -3
  77. package/src/value_object.ts +23 -19
  78. package/src/wire.ts +1 -0
  79. package/src/wire_encode.ts +48 -4
  80. package/src/workspace_routing.ts +57 -0
  81. package/src/workspace_sharding.ts +9 -9
  82. package/src/certified_read.ts +0 -313
package/src/manifest.ts CHANGED
@@ -28,9 +28,18 @@
28
28
  import { createHash } from "node:crypto";
29
29
  import { z } from "zod";
30
30
  import { encodeKernelSchema, encodeRefMode } from "./wire_encode.js";
31
+ import { encodeDriver } from "./drivers.js";
31
32
  import type { WireSchema, WireRefMode } from "./wire.js";
32
33
  import type { ReferentialMarker } from "./directive.js";
33
34
  import type { DomainModule } from "./codegen_dart.js";
35
+ import type { Field } from "./fields.js";
36
+ import {
37
+ BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
38
+ OWNED_CUSTODY_PAYLOAD_FIELD,
39
+ OWNED_KEY_PAYLOAD_FIELD,
40
+ OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
41
+ OWNED_READ_RELATION,
42
+ } from "./aggregate.js";
34
43
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
35
44
  import type { QueryDecl } from "./query.js";
36
45
  import { finishCount, type AnyCount } from "./count.js";
@@ -43,7 +52,6 @@ import type { CanonicalPred } from "./predicate.js";
43
52
  import { assertFlowGate } from "./flow_gate.js";
44
53
  import type { DerivedDecl } from "./derived.js";
45
54
  import type { CombinedDecl } from "./combined.js";
46
- import type { CertifiedReadDecl } from "./certified_read.js";
47
55
  import type { RelationDecl } from "./relation.js";
48
56
  import { type CanonicalWorkspaceType } from "./workspace_type.js";
49
57
  import {
@@ -56,6 +64,7 @@ import {
56
64
  mintFieldSid,
57
65
  type StableAggregateId,
58
66
  type StableFieldId,
67
+ type StableFieldEvolution,
59
68
  type StableIds,
60
69
  } from "./stable_ids.js";
61
70
 
@@ -107,6 +116,12 @@ export interface CanonicalAggregate {
107
116
  * so adding/changing it MOVES the hash. The scope is the aggregate's `requiresRead`.
108
117
  */
109
118
  readonly encryptedFields?: string[];
119
+ /** Secure-by-default owned lowering. Presence means `requiresRead` is a PER-RECORD relation (not a
120
+ * workspace-wide capability), `ownerField` is framework-stamped, and the encrypted slots use the
121
+ * stateful-client owned-key lane. */
122
+ readonly owned?: { readonly ownerField: string; readonly relation: string };
123
+ /** Stored framework fields intentionally hidden from generated business models. */
124
+ readonly internalFields?: readonly string[];
110
125
  /**
111
126
  * ATTRIBUTED READS (architecture/field_provenance.md) — the STORED field names whose
112
127
  * folded values keep author provenance (`.attributed()`), SORTED. PRESENT only when
@@ -152,17 +167,6 @@ export interface CanonicalDirective {
152
167
  * same omit-when-empty hash-invariance reason as {@link reads}.
153
168
  */
154
169
  readonly emits?: Record<string, { max?: number }>;
155
- /**
156
- * The directive's DECLARED CertifiedReads (survival Constraint 5; `certified_read.md`): the
157
- * profiled write-path reads its `decide()` consults, as `query_id → { sql, multiRow,
158
- * uniqueTieBreakers }`. SORTED BY `query_id` (the SET of declared reads is the contract).
159
- * Part of the domain identity: the gate DERIVES the required-read set from these `query_id`s
160
- * (the vacuous-pass close), and the SQL RECIPE + shape are captured so a recipe change moves
161
- * the hash (a different read = a different contract). OMITTED ENTIRELY when the directive
162
- * declares no write-path read — so a read-free directive is byte-identical in the manifest to
163
- * before this key existed (pinned golden hashes stay UNCHANGED).
164
- */
165
- readonly certifiedReads?: CanonicalCertifiedRead[];
166
170
  /**
167
171
  * The directive's DECLARED CAPTURED-READ queries (#58 — the captured-read lane):
168
172
  * the O(1) DSL queries its `plan` may `read()` on the write path, each carried as
@@ -185,6 +189,16 @@ export interface CanonicalDirective {
185
189
  * UNCHANGED), exactly how {@link reads}/{@link relations} stay hash-neutral.
186
190
  */
187
191
  readonly requiresHostCapability?: string[];
192
+ /**
193
+ * A law-declared business conflict resolver. The generated client carries an immutable
194
+ * framework snapshot beside this directive's payload, and every admission/replay door
195
+ * compare-and-sets it against the exact pending observation set. Omitted for ordinary
196
+ * directives, so ordinary business actions carry no conflict vocabulary.
197
+ */
198
+ readonly conflictResolution?: {
199
+ readonly aggregate: string;
200
+ readonly field: string;
201
+ };
188
202
  /**
189
203
  * The directive's DECLARED cross-workspace relations (`cross_workspace.md` §2): the relations
190
204
  * a cross-workspace PR proposing this directive is adjudicated against. Each carries the
@@ -233,15 +247,6 @@ export interface CanonicalCapturedRead {
233
247
  readonly returns: string;
234
248
  }
235
249
 
236
- /** One captured DECLARED CertifiedRead — the profiled write-path read identity in the manifest.
237
- * `queryId` is the gate's required-read key; `sql`/`multiRow`/`uniqueTieBreakers` are the recipe
238
- * + shape the admission re-run + the profile pin (a recipe change moves the domain hash). */
239
- export interface CanonicalCertifiedRead {
240
- readonly queryId: string;
241
- readonly sql: string;
242
- readonly multiRow: boolean;
243
- readonly uniqueTieBreakers: string[];
244
- }
245
250
 
246
251
  /**
247
252
  * One captured query declaration — the read-side closure's named, INDEXED read.
@@ -705,16 +710,6 @@ function canonicalEmits(
705
710
  return { emits };
706
711
  }
707
712
 
708
- /**
709
- * Canonicalize a directive's declared CertifiedReads into a manifest fragment (survival
710
- * Constraint 5). OMIT-WHEN-EMPTY: returns `{}` (no `certifiedReads` key) when the directive
711
- * declares none — the omission keeps read-free hashes unchanged. When non-empty, returns a
712
- * `certifiedReads` array SORTED BY `queryId` (the SET of declared reads is the contract), each
713
- * carrying its `queryId` + the profiled `sql`/`multiRow`/`uniqueTieBreakers` recipe (so a recipe
714
- * change moves the domain hash — a different read is a different contract). The local binding
715
- * NAME (the `reads:{}` key) is incidental to identity (it is `decide`'s ergonomics), so it is
716
- * NOT captured — only the `query_id` + recipe.
717
- */
718
713
  /**
719
714
  * Canonicalize a directive's DECLARED captured-read queries (#58) into a manifest
720
715
  * fragment. OMIT-WHEN-EMPTY: returns `{}` (no `capturedReads` key) when the directive
@@ -732,22 +727,6 @@ function canonicalCapturedReads(
732
727
  return { capturedReads };
733
728
  }
734
729
 
735
- function canonicalCertifiedReads(
736
- declared: Record<string, CertifiedReadDecl>,
737
- ): { certifiedReads?: CanonicalCertifiedRead[] } {
738
- const decls = Object.values(declared);
739
- if (decls.length === 0) return {};
740
- const certifiedReads: CanonicalCertifiedRead[] = decls
741
- .map((d) => ({
742
- queryId: d.queryId,
743
- sql: d.sql,
744
- multiRow: d.multiRow,
745
- uniqueTieBreakers: [...d.uniqueTieBreakers],
746
- }))
747
- .sort((a, b) => (a.queryId < b.queryId ? -1 : a.queryId > b.queryId ? 1 : 0));
748
- return { certifiedReads };
749
- }
750
-
751
730
  /**
752
731
  * Canonicalize a directive's declared cross-workspace relations into a manifest fragment
753
732
  * (`cross_workspace.md` §2). OMIT-WHEN-EMPTY: returns `{}` (no `relations` key) when the
@@ -1004,19 +983,31 @@ function stableIdsForModule(
1004
983
  for (const agg of aggregates) {
1005
984
  const prior = continuity?.aggregates[agg.id];
1006
985
  const aggSid = prior?.sid ?? mintAggregateSid(domainSid, agg.id);
1007
- const handleFields = (handlesById.get(agg.id)?.fields ?? {}) as Record<
1008
- string,
1009
- { kind?: string; isFromBirth?: true } | undefined
1010
- >;
986
+ const handleFields = (handlesById.get(agg.id)?.fields ?? {}) as Record<string, Field | undefined>;
1011
987
  const fields: Record<string, StableFieldId> = {};
1012
988
  for (const fieldName of Object.keys(agg.schema).sort()) {
1013
989
  const pf = prior?.fields[fieldName];
1014
990
  const kind = handleFields[fieldName]?.kind;
991
+ const handle = handleFields[fieldName];
992
+ const details = handle === undefined ? undefined : fieldValueContractDetails(handle);
993
+ const driver = handle === undefined ? undefined : encodeDriver(handle.driver);
994
+ const schema =
995
+ handle === undefined || kind === undefined || details === undefined || driver === undefined
996
+ ? undefined
997
+ : fieldSchemaIdentity(aggSid, pf?.sid ?? mintFieldSid(aggSid, fieldName), kind, details.contract, driver);
998
+ const evolutions =
999
+ handle === undefined || schema === undefined || driver === undefined || details === undefined
1000
+ ? []
1001
+ : fieldEvolutionRules(handle, pf, aggSid, pf?.sid ?? mintFieldSid(aggSid, fieldName), details, driver, schema);
1015
1002
  fields[fieldName] = {
1016
1003
  sid: pf?.sid ?? mintFieldSid(aggSid, fieldName),
1017
1004
  ...(pf?.was !== undefined && pf.was.length > 0 ? { was: pf.was } : {}),
1018
1005
  // The CURRENT kind, always from the live module (continuity never supplies it).
1019
1006
  ...(kind !== undefined ? { kind } : {}),
1007
+ ...(details !== undefined ? { contract: details.contract, shape: details.shape } : {}),
1008
+ ...(schema !== undefined ? { schema } : {}),
1009
+ ...(driver !== undefined ? { driver } : {}),
1010
+ ...(evolutions.length > 0 ? { evolutions } : {}),
1020
1011
  // Birth-presence law rides identity: the evolve guard reads it (fromBirth may not be
1021
1012
  // NEWLY declared on a sid that existed without it).
1022
1013
  ...(handleFields[fieldName]?.isFromBirth === true ? { fromBirth: true as const } : {}),
@@ -1031,25 +1022,313 @@ function stableIdsForModule(
1031
1022
  return { v: 1, domain: domainSid, aggregates: out };
1032
1023
  }
1033
1024
 
1025
+ /**
1026
+ * Identity of the value a generated TS/Dart conflict resolver is allowed to call `T`.
1027
+ * This is deliberately narrower than the domain hash (unrelated law may roll out without
1028
+ * invalidating a conflict) and richer than `Field.kind` (enum arms, value-object members,
1029
+ * nullability, references and encrypted/plain decoding all matter to the public type).
1030
+ */
1031
+ function fieldValueContractDetails(field: Field): { contract: string; shape: unknown } {
1032
+ const valueObjectContract = (meta: NonNullable<Field["valueObject"]>) => ({
1033
+ descriptor: meta.descriptor,
1034
+ dependencies: meta.dependencies,
1035
+ ...(meta.geometry !== undefined ? { geometry: meta.geometry } : {}),
1036
+ });
1037
+ const contractShape = normalizeFieldContract({
1038
+ v: "nomos-field-value-contract-v1",
1039
+ kind: field.kind,
1040
+ jsonSchema: z.toJSONSchema(field.zod as z.ZodTypeAny),
1041
+ ...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
1042
+ ...(field.jsonShape !== undefined ? { jsonShape: field.jsonShape } : {}),
1043
+ ...(field.valueObject !== undefined
1044
+ ? { valueObject: valueObjectContract(field.valueObject) }
1045
+ : {}),
1046
+ ...(field.mapValueVo !== undefined
1047
+ ? { mapValueVo: valueObjectContract(field.mapValueVo) }
1048
+ : {}),
1049
+ ...(field.refAggregateId !== undefined ? { refAggregateId: field.refAggregateId } : {}),
1050
+ ...(field.foreignRef !== undefined ? { foreignRef: field.foreignRef } : {}),
1051
+ ...(field.nullSemantic !== undefined ? { nullSemantic: field.nullSemantic } : {}),
1052
+ ...(field.isClearable === true ? { clearable: true } : {}),
1053
+ ...(field.isEncrypted === true ? { encrypted: true } : {}),
1054
+ });
1055
+ const contract = createHash("sha256")
1056
+ .update(
1057
+ `nomos-field-value-contract:v1:${canonicalJson(contractShape)}`,
1058
+ "utf8",
1059
+ )
1060
+ .digest("hex");
1061
+ return { contract, shape: contractShape };
1062
+ }
1063
+
1064
+ function fieldSchemaIdentity(
1065
+ aggregateSid: string,
1066
+ fieldSid: string,
1067
+ kind: string,
1068
+ valueContract: string,
1069
+ driver: unknown,
1070
+ ): string {
1071
+ return createHash("sha256")
1072
+ .update(
1073
+ canonicalJson({
1074
+ v: "nomos-field-schema-v2",
1075
+ aggregate: aggregateSid,
1076
+ field: fieldSid,
1077
+ kind,
1078
+ valueContract,
1079
+ driver,
1080
+ }),
1081
+ "utf8",
1082
+ )
1083
+ .digest("hex");
1084
+ }
1085
+
1086
+ function fieldEvolutionRules(
1087
+ field: Field,
1088
+ prior: StableFieldId | undefined,
1089
+ aggregateSid: string,
1090
+ fieldSid: string,
1091
+ target: { contract: string; shape: unknown },
1092
+ driver: unknown,
1093
+ targetSchema: string,
1094
+ ): StableFieldEvolution[] {
1095
+ const bySource = new Map<string, StableFieldEvolution>();
1096
+ const add = (rule: StableFieldEvolution, origin: string) => {
1097
+ if (rule.from === rule.to) return;
1098
+ const existing = bySource.get(rule.from);
1099
+ if (existing !== undefined && canonicalJson(existing) !== canonicalJson(rule)) {
1100
+ throw new Error(
1101
+ `field evolution collision for source contract ${rule.from}: ${origin} disagrees with an existing route. ` +
1102
+ `One source contract must have exactly one deterministic destination.`,
1103
+ );
1104
+ }
1105
+ bySource.set(rule.from, rule);
1106
+ };
1107
+
1108
+ for (const rule of prior?.evolutions ?? []) add(rule, "the prior release");
1109
+
1110
+ for (const declaration of field.evolutions ?? []) {
1111
+ const source = fieldValueContractDetails(declaration.source);
1112
+ const sourceSchema = fieldSchemaIdentity(
1113
+ aggregateSid,
1114
+ fieldSid,
1115
+ declaration.source.kind,
1116
+ source.contract,
1117
+ driver,
1118
+ );
1119
+ if (declaration.mode === "identity" && !safeIdentityProjection(source.shape, target.shape)) {
1120
+ throw new Error(
1121
+ `.evolvesFrom(source) is not a safe identity widening: some source values are not valid ` +
1122
+ `under the target field. Supply a finite business transform as the second argument.`,
1123
+ );
1124
+ }
1125
+ add(
1126
+ {
1127
+ from: sourceSchema,
1128
+ to: targetSchema,
1129
+ source: {
1130
+ kind: declaration.source.kind,
1131
+ ...(declaration.source.mapValueKind !== undefined
1132
+ ? { mapValueKind: declaration.source.mapValueKind }
1133
+ : {}),
1134
+ },
1135
+ mode: declaration.mode,
1136
+ ...(declaration.cases !== undefined ? { cases: declaration.cases } : {}),
1137
+ },
1138
+ ".evolvesFrom()",
1139
+ );
1140
+ }
1141
+
1142
+ // The compiler silently owns genuinely compatible widening between consecutive releases.
1143
+ // The prior lock carries the full value descriptor, so this is a proof, not a cast.
1144
+ if (
1145
+ prior?.schema !== undefined &&
1146
+ prior.schema !== targetSchema &&
1147
+ prior.shape !== undefined &&
1148
+ canonicalJson(prior.driver) === canonicalJson(driver) &&
1149
+ safeAutomaticWidening(prior.shape, target.shape) &&
1150
+ !bySource.has(prior.schema)
1151
+ ) {
1152
+ add(
1153
+ {
1154
+ from: prior.schema,
1155
+ to: targetSchema,
1156
+ source: {
1157
+ kind: prior.kind ?? field.kind,
1158
+ ...((prior.shape as { mapValueKind?: string } | undefined)?.mapValueKind !== undefined
1159
+ ? { mapValueKind: (prior.shape as { mapValueKind: string }).mapValueKind }
1160
+ : {}),
1161
+ },
1162
+ mode: "identity",
1163
+ },
1164
+ "the compiler's compatible-widening proof",
1165
+ );
1166
+ }
1167
+
1168
+ // Every retained source route must reach the one current target. This is the compile-time
1169
+ // completeness gate for all still-supported eras, not merely the immediately prior law.
1170
+ for (const source of bySource.keys()) {
1171
+ const seen = new Set<string>();
1172
+ let cursor = source;
1173
+ while (cursor !== targetSchema) {
1174
+ if (seen.has(cursor)) {
1175
+ throw new Error(`field evolution graph cycles at ${cursor}; every source must terminate at ${targetSchema}`);
1176
+ }
1177
+ seen.add(cursor);
1178
+ const next = bySource.get(cursor);
1179
+ if (next === undefined) {
1180
+ throw new Error(
1181
+ `field evolution route from ${source} stops at ${cursor}, not the current contract ${targetSchema}. ` +
1182
+ `Carry the intermediate source with .evolvesFrom(...) before this rollout.`,
1183
+ );
1184
+ }
1185
+ cursor = next.to;
1186
+ }
1187
+ }
1188
+
1189
+ return [...bySource.values()].sort((a, b) => (a.from < b.from ? -1 : a.from > b.from ? 1 : 0));
1190
+ }
1191
+
1192
+ /** Explicit identity projection: the tenant declares the business meaning is continuous;
1193
+ * the compiler still proves every old value is accepted and its physical decoding is unchanged. */
1194
+ function safeIdentityProjection(sourceShape: unknown, targetShape: unknown): boolean {
1195
+ if (sourceShape === null || targetShape === null || typeof sourceShape !== "object" || typeof targetShape !== "object") {
1196
+ return canonicalJson(sourceShape) === canonicalJson(targetShape);
1197
+ }
1198
+ const source = sourceShape as Record<string, unknown>;
1199
+ const target = targetShape as Record<string, unknown>;
1200
+ const physical = (shape: Record<string, unknown>) => {
1201
+ const { kind, mapValueKind, jsonShape, encrypted } = shape;
1202
+ return { kind, mapValueKind, jsonShape, encrypted };
1203
+ };
1204
+ return canonicalJson(physical(source)) === canonicalJson(physical(target))
1205
+ && jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
1206
+ }
1207
+
1208
+ /** Automatic widening is stricter: every non-schema business descriptor must remain
1209
+ * identical. Brands, units, references and null meaning require an explicit declaration. */
1210
+ function safeAutomaticWidening(sourceShape: unknown, targetShape: unknown): boolean {
1211
+ if (sourceShape === null || targetShape === null || typeof sourceShape !== "object" || typeof targetShape !== "object") {
1212
+ return canonicalJson(sourceShape) === canonicalJson(targetShape);
1213
+ }
1214
+ const source = sourceShape as Record<string, unknown>;
1215
+ const target = targetShape as Record<string, unknown>;
1216
+ const metadata = (shape: Record<string, unknown>) => {
1217
+ const { jsonSchema: _jsonSchema, ...rest } = shape;
1218
+ return rest;
1219
+ };
1220
+ return canonicalJson(metadata(source)) === canonicalJson(metadata(target))
1221
+ && jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
1222
+ }
1223
+
1224
+ function jsonSchemaSubset(source: unknown, target: unknown): boolean {
1225
+ if (canonicalJson(source) === canonicalJson(target)) return true;
1226
+ if (source === null || target === null || typeof source !== "object" || typeof target !== "object") return false;
1227
+ const s = source as Record<string, unknown>;
1228
+ const t = target as Record<string, unknown>;
1229
+ if (Array.isArray(s.enum)) return s.enum.every((value) => jsonSchemaAcceptsLiteral(t, value));
1230
+ if (Object.prototype.hasOwnProperty.call(s, "const")) return jsonSchemaAcceptsLiteral(t, s.const);
1231
+ const sUnion = (Array.isArray(s.anyOf) ? s.anyOf : Array.isArray(s.oneOf) ? s.oneOf : undefined);
1232
+ if (sUnion !== undefined) return sUnion.every((arm) => jsonSchemaSubset(arm, target));
1233
+ const tUnion = (Array.isArray(t.anyOf) ? t.anyOf : Array.isArray(t.oneOf) ? t.oneOf : undefined);
1234
+ if (tUnion !== undefined) return tUnion.some((arm) => jsonSchemaSubset(source, arm));
1235
+ const sourceTypes = new Set(Array.isArray(s.type) ? s.type : s.type === undefined ? [] : [s.type]);
1236
+ const targetTypes = new Set(Array.isArray(t.type) ? t.type : t.type === undefined ? [] : [t.type]);
1237
+ if ([...sourceTypes].some((type) => !targetTypes.has(type))) return false;
1238
+ if (sourceTypes.has("object")) {
1239
+ const sp = (s.properties ?? {}) as Record<string, unknown>;
1240
+ const tp = (t.properties ?? {}) as Record<string, unknown>;
1241
+ const sr = new Set(Array.isArray(s.required) ? s.required as string[] : []);
1242
+ const tr = new Set(Array.isArray(t.required) ? t.required as string[] : []);
1243
+ if ([...tr].some((name) => !sr.has(name))) return false;
1244
+ for (const [name, schema] of Object.entries(sp)) {
1245
+ if (tp[name] === undefined) {
1246
+ if (t.additionalProperties === false) return false;
1247
+ } else if (!jsonSchemaSubset(schema, tp[name])) {
1248
+ return false;
1249
+ }
1250
+ }
1251
+ if (s.additionalProperties !== false && t.additionalProperties === false) return false;
1252
+ }
1253
+ if (sourceTypes.has("array") && s.items !== undefined && t.items !== undefined) {
1254
+ if (!jsonSchemaSubset(s.items, t.items)) return false;
1255
+ }
1256
+ return sourceTypes.size > 0 && [...sourceTypes].every((type) => targetTypes.has(type));
1257
+ }
1258
+
1259
+ function jsonSchemaAcceptsLiteral(schema: Record<string, unknown>, value: unknown): boolean {
1260
+ if (Array.isArray(schema.enum)) return schema.enum.some((candidate) => canonicalJson(candidate) === canonicalJson(value));
1261
+ if (Object.prototype.hasOwnProperty.call(schema, "const")) return canonicalJson(schema.const) === canonicalJson(value);
1262
+ const union = Array.isArray(schema.anyOf) ? schema.anyOf : Array.isArray(schema.oneOf) ? schema.oneOf : undefined;
1263
+ if (union !== undefined) return union.some((arm) => arm !== null && typeof arm === "object" && jsonSchemaAcceptsLiteral(arm as Record<string, unknown>, value));
1264
+ const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value === "number" ? "number" : typeof value;
1265
+ const types = new Set(Array.isArray(schema.type) ? schema.type : schema.type === undefined ? [] : [schema.type]);
1266
+ return types.has(actual) || (actual === "number" && types.has("integer") && Number.isInteger(value));
1267
+ }
1268
+
1269
+ /** JSON Schema contains a handful of set-valued arrays whose authored order is not part
1270
+ * of the accepted value language. Normalize those (and VO enum `values`) so a cosmetic
1271
+ * enum/union reorder does not manufacture an N/N+1 incompatibility. */
1272
+ function normalizeFieldContract(value: unknown, parentKey = ""): unknown {
1273
+ if (Array.isArray(value)) {
1274
+ const items = value.map((item) => normalizeFieldContract(item));
1275
+ if (["enum", "required", "type", "values", "oneOf", "anyOf", "allOf"].includes(parentKey)) {
1276
+ return items.sort((a, b) => {
1277
+ const left = canonicalJson(a);
1278
+ const right = canonicalJson(b);
1279
+ return left < right ? -1 : left > right ? 1 : 0;
1280
+ });
1281
+ }
1282
+ return items;
1283
+ }
1284
+ if (value !== null && typeof value === "object") {
1285
+ return Object.fromEntries(
1286
+ Object.entries(value as Record<string, unknown>).map(([key, child]) => [
1287
+ key,
1288
+ normalizeFieldContract(child, key),
1289
+ ]),
1290
+ );
1291
+ }
1292
+ return value;
1293
+ }
1294
+
1034
1295
  /**
1035
1296
  * Build the canonical semantic manifest for a domain module — purely from the
1036
1297
  * in-memory DSL objects. No file reads, no bundle, no esbuild: the manifest is a
1037
1298
  * function of the module alone, so it is bundler- AND location-independent.
1038
1299
  */
1039
1300
  export function domainManifest(mod: DomainModule): CanonicalManifest {
1301
+ for (const agg of mod.aggregates) {
1302
+ if (agg.audience === undefined) {
1303
+ throw new Error(
1304
+ `aggregate '${agg.id}' has no audience. Nomos will not guess who may read business state: ` +
1305
+ `finish the declaration with .owned(), .readableBy("<business relation>"), or the deliberate ` +
1306
+ `.public() exception.`,
1307
+ );
1308
+ }
1309
+ }
1310
+ for (const directive of mod.directives) {
1311
+ const shape = zodObjectShape(directive.payloadSchema as ZodLike);
1312
+ if (shape === undefined) continue;
1313
+ for (const reserved of [
1314
+ OWNED_KEY_PAYLOAD_FIELD,
1315
+ OWNED_CUSTODY_PAYLOAD_FIELD,
1316
+ OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
1317
+ BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
1318
+ ]) {
1319
+ if (!Object.prototype.hasOwnProperty.call(shape, reserved)) continue;
1320
+ const frameworkBirthCarrier = reserved === BUSINESS_BIRTH_SEED_PAYLOAD_FIELD
1321
+ && (directive as { businessBirth?: unknown }).businessBirth !== undefined;
1322
+ if (frameworkBirthCarrier) continue;
1323
+ throw new Error(
1324
+ `directive '${directive.id}': payload field '${reserved}' is reserved by Nomos ownership. ` +
1325
+ `Sorry mate, that concept is already taken by the framework; give the business payload field ` +
1326
+ `a different name.`,
1327
+ );
1328
+ }
1329
+ }
1040
1330
  const aggregates: CanonicalAggregate[] = mod.aggregates
1041
1331
  .map((agg) => {
1042
- // SECURE-BY-DEFAULT — `.owned()` is declared in the audience API, but its owner-SCOPED reads
1043
- // (the projection row-filter) + per-record E2E are not yet enforced. Refuse to compile owned law
1044
- // FAIL-CLOSED rather than ship a type whose reads aren't gated (architecture/secure_by_default_dsl.md
1045
- // build order). Use `.public()` or `.readableBy(cap)` until the `.owned()` slice lands.
1046
- if (agg.audience?.kind === "owned") {
1047
- throw new Error(
1048
- `aggregate '${agg.id}': .owned() is declared but its read-scoping + E2E are not yet enforced — ` +
1049
- `refusing to compile owned law (fail-closed). Use .public() or .readableBy("<cap>") for now ` +
1050
- `(the .owned() slice is next — see architecture/secure_by_default_dsl.md).`,
1051
- );
1052
- }
1053
1332
  return {
1054
1333
  id: agg.id,
1055
1334
  schema: sortedSchema(encodeKernelSchema(agg)),
@@ -1065,6 +1344,10 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
1065
1344
  // E2E ENCRYPTION (slice 2) — the encrypted field names. Hash-bearing law;
1066
1345
  // omit-when-none so plaintext aggregates are byte-stable.
1067
1346
  ...(agg.encryptedFields !== undefined ? { encryptedFields: [...agg.encryptedFields] } : {}),
1347
+ ...(agg.audience?.kind === "owned"
1348
+ ? { owned: { ownerField: agg.audience.ownerField, relation: OWNED_READ_RELATION } }
1349
+ : {}),
1350
+ ...(agg.internalFields !== undefined ? { internalFields: [...agg.internalFields].sort() } : {}),
1068
1351
  // ATTRIBUTED READS — the attributed field names (the driver wrap in `schema` is
1069
1352
  // the hash-bearing law; this mirror is what codegen reads). Omit-when-none.
1070
1353
  ...(agg.attributedFields !== undefined ? { attributedFields: [...agg.attributedFields] } : {}),
@@ -1072,6 +1355,9 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
1072
1355
  })
1073
1356
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
1074
1357
 
1358
+ const conflictResolutionByDirective = new Map(
1359
+ (mod.surfacedResolvers ?? []).map((binding) => [binding.directiveId, binding] as const),
1360
+ );
1075
1361
  const directives: CanonicalDirective[] = mod.directives
1076
1362
  .map((d) => ({
1077
1363
  id: d.id,
@@ -1085,9 +1371,16 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
1085
1371
  ...canonicalReads(d.declaredReads),
1086
1372
  ...canonicalCapturedReads(d.declaredQueryReads ?? []),
1087
1373
  ...canonicalEmits(d.declaredEmits),
1088
- ...canonicalCertifiedReads(d.declaredCertifiedReads),
1089
1374
  ...canonicalRelations(d.declaredRelations),
1090
1375
  ...canonicalHostCapabilities(d.declaredHostCapabilities),
1376
+ ...(conflictResolutionByDirective.has(d.id)
1377
+ ? {
1378
+ conflictResolution: {
1379
+ aggregate: conflictResolutionByDirective.get(d.id)!.aggregateId,
1380
+ field: conflictResolutionByDirective.get(d.id)!.field,
1381
+ },
1382
+ }
1383
+ : {}),
1091
1384
  }))
1092
1385
  .sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
1093
1386
 
@@ -61,6 +61,14 @@ export function qualifyManifestAggregateLabels(
61
61
  directives: manifest.directives.map((d) => ({
62
62
  ...d,
63
63
  ...(namespaced.has(d.target) ? { target: qa(d.target) } : {}),
64
+ ...(d.conflictResolution !== undefined && namespaced.has(d.conflictResolution.aggregate)
65
+ ? {
66
+ conflictResolution: {
67
+ ...d.conflictResolution,
68
+ aggregate: qa(d.conflictResolution.aggregate),
69
+ },
70
+ }
71
+ : {}),
64
72
  ...(d.capturedReads !== undefined
65
73
  ? {
66
74
  capturedReads: d.capturedReads.map((r) =>
@@ -72,10 +72,14 @@ export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set
72
72
  // domain lifecycle (the law records themselves)
73
73
  "PolicyBundle",
74
74
  "DomainInstallation",
75
+ "nomos_internal_ReleaseReceipt_6f0d3a",
75
76
  // encryption identity + scope (the operator-blind key machinery)
76
77
  "EncKey",
77
78
  "KeyScope",
78
79
  "WrappedKey",
80
+ // framework-owned private-data recovery policy + durable recovery order
81
+ "NomosOwnedRecoveryPolicy",
82
+ "NomosOwnedRecoveryOrder",
79
83
  // genesis / births
80
84
  "GenesisWarrant",
81
85
  // metered birth (slice A) — NomosBirthRecord is kernel-read by the birth-record
package/src/relation.ts CHANGED
@@ -10,7 +10,7 @@
10
10
  // the source/target aggregate endpoints, the BOUNDED evidence read it discloses (the
11
11
  // disclosure contract + anti-smuggle key, §2.4), and that an invariant predicate guards it.
12
12
  //
13
- // This is DECLARATION ONLY — symmetric with `emits`/`requires`/`readsCertified`. It lowers
13
+ // This is DECLARATION ONLY — symmetric with `emits`/`requires`/`reads`. It lowers
14
14
  // into the domain manifest IR (`manifest.ts`, the hash-pinned #136 identity), which the ONE
15
15
  // write-path gate (`executor::admit` step 5.5 / `kernel::manifest_view`) resolves to adjudicate
16
16
  // a carried PR against the target's OWN law. The invariant PREDICATE itself is executable
@@ -63,6 +63,46 @@ export interface DomainInterface {
63
63
  >;
64
64
  }
65
65
 
66
+ /** Remove framework-managed aggregate fields from the client-facing structural interface. Stable ids for
67
+ * those fields remain in the law/lockfile; clients negotiate only the business shape they can see. */
68
+ export function publicClientInterface(
69
+ modules: readonly DomainModule[],
70
+ offered: Readonly<Record<string, DomainInterface>>,
71
+ ): Record<string, DomainInterface> {
72
+ const hidden = new Map<string, Map<string, Set<string>>>();
73
+ for (const mod of modules) {
74
+ const domain = mod.domain ?? mod.name;
75
+ const byAggregate = hidden.get(domain) ?? new Map<string, Set<string>>();
76
+ hidden.set(domain, byAggregate);
77
+ for (const aggregate of mod.aggregates ?? []) {
78
+ const fields = (aggregate as { internalFields?: readonly string[] }).internalFields ?? [];
79
+ if (fields.length > 0) byAggregate.set(aggregate.id, new Set(fields));
80
+ }
81
+ }
82
+ const result: Record<string, DomainInterface> = {};
83
+ for (const [domain, value] of Object.entries(offered)) {
84
+ const byAggregate = hidden.get(domain);
85
+ if (byAggregate === undefined || byAggregate.size === 0) {
86
+ result[domain] = value;
87
+ continue;
88
+ }
89
+ const aggregates: Record<string, AggregateReadShape> = {};
90
+ for (const [sid, aggregate] of Object.entries(value.aggregates)) {
91
+ const fields = byAggregate.get(aggregate.label);
92
+ aggregates[sid] = fields === undefined
93
+ ? aggregate
94
+ : {
95
+ ...aggregate,
96
+ fields: Object.fromEntries(
97
+ Object.entries(aggregate.fields).filter(([, field]) => !fields.has(field.label)),
98
+ ),
99
+ };
100
+ }
101
+ result[domain] = { ...value, aggregates };
102
+ }
103
+ return result;
104
+ }
105
+
66
106
  /** The verdict: compatible iff nothing is missing and nothing is type-incompatible.
67
107
  * `missing` = a depended-on directive/aggregate/field the workspace doesn't expose;
68
108
  * `incompatible` = present but type-divergent (a payload field kind, a retyped read field). */
@@ -157,14 +197,19 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
157
197
  Object.assign(directives, sortedRec(dirEntries));
158
198
  const aggregates: Record<string, AggregateReadShape> = {};
159
199
  const aggEntries: Array<[string, AggregateReadShape]> = [];
160
- for (const agg of (mod.aggregates ?? []) as Array<{ id: string; fields: Record<string, Field> }>) {
200
+ for (const agg of (mod.aggregates ?? []) as Array<{
201
+ id: string;
202
+ fields: Record<string, Field>;
203
+ internalFields?: readonly string[];
204
+ }>) {
161
205
  if (isInternal(agg)) continue;
162
206
  const label = agg.id;
163
207
  const sa = stableIds?.aggregates?.[label];
164
208
  const aggSid = sa?.sid ?? label;
165
209
  const fieldEntries: Array<[string, { kind: string; label: string }]> = [];
210
+ const internal = new Set(agg.internalFields ?? []);
166
211
  for (const [fname, f] of Object.entries(agg.fields ?? {})) {
167
- if (!f || f.kind === "hasMany") continue;
212
+ if (!f || f.kind === "hasMany" || internal.has(fname)) continue;
168
213
  const fsid = sa?.fields?.[fname]?.sid ?? fname;
169
214
  const kind = sa?.fields?.[fname]?.kind ?? aggFieldReadKind(f);
170
215
  fieldEntries.push([fsid, { kind, label: fname }]);