@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.
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
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 = ${
|
|
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 = ${
|
|
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 = ${
|
|
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,
|
|
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)
|
|
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.
|