@githolon/dsl 0.102.2 → 0.102.3

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.102.2",
3
+ "version": "0.102.3",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -31,6 +31,7 @@ import {
31
31
  } from "./framework/framework_aggregates.js";
32
32
  import { isBusinessIntent } from "./intent.js";
33
33
  import { contractTypeName } from "./contract.js";
34
+ import { admitsNull } from "./null_meta.js";
34
35
  import type {
35
36
  WorkspaceSharingPolicy,
36
37
  WorkspaceSharingReceipt,
@@ -1051,7 +1052,10 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1051
1052
  ? sourceFieldMember.slice(0, -2)
1052
1053
  : `${sourceFieldMember}Ref`;
1053
1054
  const method = member(stem);
1054
- const memberNullable = field.isFromBirth !== true;
1055
+ const memberNullable = admitsNull(field) || field.isFromBirth !== true;
1056
+ const sourceFieldValue = admitsNull(field)
1057
+ ? `record.${sourceFieldMember}.valueOrNull`
1058
+ : `record.${sourceFieldMember}`;
1055
1059
  const isSameDomain = sameDomainRefs.some(([name]) => name === fieldName);
1056
1060
  const isCrossDomain = crossDomainRefs.some(([name]) => name === fieldName);
1057
1061
  if (!isSameDomain && !isCrossDomain) continue;
@@ -1064,7 +1068,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1064
1068
  ` Future<${targetModel}?> ${method}(${sourceModel} record) async {`,
1065
1069
  ...(memberNullable
1066
1070
  ? [
1067
- ` final id = record.${sourceFieldMember};`,
1071
+ ` final id = ${sourceFieldValue};`,
1068
1072
  ` if (id == null) return null;`,
1069
1073
  ` final rows = await _client.${targetById}(NmsInternalNomosRefs.id(id));`,
1070
1074
  ]
@@ -1095,7 +1099,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1095
1099
  ` Future<${targetModel}?> ${method}(${sourceModel} record) async {`,
1096
1100
  ...(memberNullable
1097
1101
  ? [
1098
- ` final id = record.${sourceFieldMember};`,
1102
+ ` final id = ${sourceFieldValue};`,
1099
1103
  ` if (id == null) return null;`,
1100
1104
  ` final application = await ${opener}(`,
1101
1105
  ` _client.bridge,`,
@@ -1107,7 +1111,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1107
1111
  ` );`,
1108
1112
  ]
1109
1113
  : [
1110
- ` final id = record.${sourceFieldMember};`,
1114
+ ` final id = ${sourceFieldValue};`,
1111
1115
  ` final application = await ${opener}(`,
1112
1116
  ` _client.bridge,`,
1113
1117
  ` id,`,
@@ -1126,7 +1130,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1126
1130
  ` Future<${targetApplication}?> ${applicationMethod}(${sourceModel} record) {`,
1127
1131
  ...(memberNullable
1128
1132
  ? [
1129
- ` final id = record.${sourceFieldMember};`,
1133
+ ` final id = ${sourceFieldValue};`,
1130
1134
  ` if (id == null) return Future<${targetApplication}?>.value(null);`,
1131
1135
  ` return ${opener}(`,
1132
1136
  ` _client.bridge,`,
@@ -1151,12 +1155,15 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1151
1155
  ? contractTypeName(foreign.targetAggregateId)
1152
1156
  : concreteClassName(foreign.targetAggregateId);
1153
1157
  const fieldMember = member(fieldName);
1158
+ const fieldValue = admitsNull(field)
1159
+ ? `record.${fieldMember}.valueOrNull`
1160
+ : `record.${fieldMember}`;
1154
1161
  const stem = fieldMember.endsWith("Id") ? fieldMember.slice(0, -2) : fieldMember;
1155
1162
  const method = member(`${stem}Of`);
1156
1163
  addCollectionMember(aggregate.id, method, [
1157
1164
  ` /// Follow [record]'s ${fieldName} to its typed business record.`,
1158
1165
  ` Future<${target}?> ${method}(${sourceModel} record) async {`,
1159
- ` final reference = record.${fieldMember};`,
1166
+ ` final reference = ${fieldValue};`,
1160
1167
  ` if (reference == null) return null;`,
1161
1168
  ` final target = await NmsInternalForeignRefs.resolve(reference, _client.bridge.foreignReader());`,
1162
1169
  ` if (target == null) return null;`,
@@ -1179,7 +1186,7 @@ export function generateDartApplication(mod: DomainModule, opts: DartApplication
1179
1186
  addCollectionMember(aggregate.id, applicationMethod, [
1180
1187
  ` /// Open the business application containing [record]'s ${fieldName}.`,
1181
1188
  ` Future<${targetApplication}?> ${applicationMethod}(${sourceModel} record) {`,
1182
- ` final reference = record.${fieldMember};`,
1189
+ ` final reference = ${fieldValue};`,
1183
1190
  ` if (reference == null) return Future<${targetApplication}?>.value(null);`,
1184
1191
  ` return ${opener}(`,
1185
1192
  ` _client.bridge,`,
@@ -62,6 +62,7 @@ import { buildNomosDescribeModel, type NomosDescribeModel } from "./describe_mod
62
62
  import { assertModelClientMemberAvailable } from "./generated_client_names.js";
63
63
  import { ownedRecordActionDirectiveId } from "./framework/owned_custody.js";
64
64
  import { contractTypeName } from "./contract.js";
65
+ import { admitsNull } from "./null_meta.js";
65
66
 
66
67
  const camel = (s: string) => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
67
68
  const lcFirst = (s: string) => (s.length ? s[0]!.toLowerCase() + s.slice(1) : s);
@@ -1268,14 +1269,15 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1268
1269
  // the DSL schema's `.optional()` — a partial fold decodes every scalar `null` when absent
1269
1270
  // (`emitReadModel`'s own doc: "PARTIAL folds"), so the member is nullable UNLESS the field is
1270
1271
  // `.fromBirth()` (birth-presence law — the ONE case the projection types non-null).
1271
- const memberNullable = f.isFromBirth !== true;
1272
+ const memberNullable = admitsNull(f) || f.isFromBirth !== true;
1273
+ const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
1272
1274
  lines.push(
1273
1275
  ` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType}) by dispatching to`,
1274
1276
  ` /// [${className}.${byIdMethod}] — the existing by-id read lane, never a second one.`,
1275
1277
  ` Future<${targetModel}?> ${method}(${className} client) async {`,
1276
1278
  ...(memberNullable
1277
1279
  ? [
1278
- ` final id = ${member};`,
1280
+ ` final id = ${memberValue};`,
1279
1281
  ` if (id == null) return null;`,
1280
1282
  ` final rows = await client.${byIdMethod}(NmsInternalNomosRefs.id(id));`,
1281
1283
  ]
@@ -1301,7 +1303,8 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1301
1303
  const member = dartMemberName(fieldName);
1302
1304
  const methodStem = member.length > 2 && member.endsWith("Id") ? member.slice(0, -2) : `${member}Ref`;
1303
1305
  const method = safeMethod(methodStem);
1304
- const memberNullable = f.isFromBirth !== true;
1306
+ const memberNullable = admitsNull(f) || f.isFromBirth !== true;
1307
+ const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
1305
1308
  lines.push(
1306
1309
  ` /// Resolve \`${fieldName}\` (${agg.id}.${fieldName} -> ${targetType}, a CROSS-DOMAIN ref) by`,
1307
1310
  ` /// dispatching to [${targetClient}.${byIdMethod}] on the target domain's client — same by-id`,
@@ -1310,7 +1313,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1310
1313
  ` Future<${targetModel}?> ${method}(${targetClient} client) async {`,
1311
1314
  ...(memberNullable
1312
1315
  ? [
1313
- ` final id = ${member};`,
1316
+ ` final id = ${memberValue};`,
1314
1317
  ` if (id == null) return null;`,
1315
1318
  ` final rows = await client.${byIdMethod}(NmsInternalNomosRefs.id(id));`,
1316
1319
  ]
@@ -1341,6 +1344,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1341
1344
  }
1342
1345
  const targetConcrete = concreteClassName(targetType);
1343
1346
  const member = dartMemberName(fieldName);
1347
+ const memberValue = admitsNull(f) ? `${member}.valueOrNull` : member;
1344
1348
  // The stored locator member is a typed `ForeignRef<Target>?` getter, so the resolver takes a
1345
1349
  // `remote…` name to avoid colliding with it (and to read as "the far side of the reference").
1346
1350
  const method = safeMethod(`remote${cap(camel(member))}`);
@@ -1350,7 +1354,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
1350
1354
  ` /// implementation opens the foreign workspace on demand; tenant code sees only this business method.`,
1351
1355
  ` /// Null locator (absent / empty id or workspace) ⇒ null.`,
1352
1356
  ` Future<${targetConcrete}?> ${method}(NomosEngine engine, {String? cloud}) {`,
1353
- ` final ref = ${member};`,
1357
+ ` final ref = ${memberValue};`,
1354
1358
  ` if (ref == null) return Future<${targetConcrete}?>.value(null);`,
1355
1359
  ` return NmsInternalForeignRefs.resolve(ref, engine.foreignReader(cloud: cloud));`,
1356
1360
  ` }`,
package/src/law_check.ts CHANGED
@@ -729,7 +729,7 @@ function dottedQueryKeyFindings(mod: DomainModule, aggById: ReadonlyMap<string,
729
729
  // admits before a null is lawful (t.optional / t.clearable / t.nullable / t.json({null})). These
730
730
  // twins pre-fire the three refusals the gate would raise: (a) a payload field that ADMITS null with
731
731
  // no null-admitting receiver; (b) a null-admitting field used where a null is structurally illegal
732
- // (authority gate, identity ref, spatial index, read key); (c) a plan op writing literal null into a
732
+ // (authority gate, spatial index, read key); (c) a plan op writing literal null into a
733
733
  // field that never declared null a meaning. All PAYLOAD-INDEPENDENT (pure declarations) ⇒ REFUSE.
734
734
 
735
735
  /** The verbatim null-semantics guidance (`null_semantics.md`) — the `fix` on twin N-a/N-c. */
@@ -843,7 +843,10 @@ function nullBanFindings(mod: DomainModule, aggById: ReadonlyMap<string, Aggrega
843
843
  });
844
844
  };
845
845
 
846
- // Facet gates (authority/standing) + identity refs — a null in these breaks the very thing they decide.
846
+ // Facet gates (authority/standing) — a null in these breaks the very thing they decide. A plain
847
+ // typed ref is an association, not structural identity: clearing `asset.spaceId`, for example,
848
+ // means the asset is no longer in a space. If a ref participates in a query/group/spatial key the
849
+ // corresponding structural check below still refuses it.
847
850
  for (const agg of mod.aggregates) {
848
851
  for (const [name, f0] of Object.entries(agg.fields)) {
849
852
  const f = f0 as Field;
@@ -851,9 +854,6 @@ function nullBanFindings(mod: DomainModule, aggById: ReadonlyMap<string, Aggrega
851
854
  if (f.facet === "authority" || f.facet === "standing") {
852
855
  ban(agg.id, name, "an authority/standing gate", "drop the null semantic — an authority sink must always resolve to a subject.");
853
856
  }
854
- if (f.kind === "ref") {
855
- ban(agg.id, name, "an identity reference", "an identity ref must always point somewhere — model absence with a separate presence flag, not a null ref.");
856
- }
857
857
  }
858
858
  }
859
859
  // Read keys / group-bys / spatial indexes — a null key row is a silent dead index.