@githolon/dsl 0.108.1 → 0.108.2

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.108.1",
3
+ "version": "0.108.2",
4
4
  "type": "module",
5
5
  "sideEffects": false,
6
6
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
@@ -211,10 +211,20 @@ export const RelationshipSubjectValue = valueObject("RelationshipSubject", {
211
211
  ownedCustody: OwnedCustodyValue.optional(),
212
212
  });
213
213
 
214
+ // Historical RelationTuple law declared this map value as `t.json()`, but the
215
+ // kernel has always admitted and stored only the nominal RelationshipSubject
216
+ // palette for this framework field. Keep that exact prior declaration as the
217
+ // source of the explicit evolution edge: this is a type-disclosure refinement
218
+ // of existing structured custody, not a JSON parser or a business-data cast.
219
+ const LegacyRelationshipSubjects = t.map(t.json()).merge(MapOf(Lww));
220
+
214
221
  export const RelationTuple = aggregate("RelationTuple", {
215
222
  object: t.string().merge(Lww),
216
223
  relation: t.string().merge(Lww),
217
- subjects: t.map(RelationshipSubjectValue.field()).merge(MapOf(Lww)),
224
+ subjects: t
225
+ .map(RelationshipSubjectValue.field())
226
+ .merge(MapOf(Lww))
227
+ .evolvesFrom(LegacyRelationshipSubjects),
218
228
  }).public();
219
229
  export type RelationTupleHandle = typeof RelationTuple;
220
230
 
@@ -50,7 +50,16 @@ export function stableIdsForModule(mod: DomainModule): StableIds {
50
50
  const evolutions =
51
51
  handle === undefined || schema === undefined || driver === undefined || details === undefined
52
52
  ? []
53
- : fieldEvolutionRules(handle, pf, aggSid, pf?.sid ?? mintFieldSid(aggSid, fieldName), details, driver, schema);
53
+ : fieldEvolutionRules(
54
+ handle,
55
+ pf,
56
+ aggSid,
57
+ pf?.sid ?? mintFieldSid(aggSid, fieldName),
58
+ details,
59
+ driver,
60
+ schema,
61
+ { aggregateId: agg.id, fieldName },
62
+ );
54
63
  fields[fieldName] = {
55
64
  sid: pf?.sid ?? mintFieldSid(aggSid, fieldName),
56
65
  ...(pf?.was !== undefined && pf.was.length > 0 ? { was: pf.was } : {}),
@@ -147,6 +156,7 @@ function fieldEvolutionRules(
147
156
  target: { contract: string; shape: unknown },
148
157
  driver: unknown,
149
158
  targetSchema: string,
159
+ identity: { aggregateId: string; fieldName: string },
150
160
  ): StableFieldEvolution[] {
151
161
  const bySource = new Map<string, StableFieldEvolution>();
152
162
  const add = (rule: StableFieldEvolution, origin: string) => {
@@ -172,7 +182,10 @@ function fieldEvolutionRules(
172
182
  source.contract,
173
183
  encodeDriver(declaration.source.driver),
174
184
  );
175
- if (declaration.mode === "identity" && !safeIdentityProjection(source.shape, target.shape)) {
185
+ if (
186
+ declaration.mode === "identity" &&
187
+ !safeIdentityProjection(source.shape, target.shape, identity)
188
+ ) {
176
189
  throw new Error(
177
190
  `.evolvesFrom(source) is not a safe identity widening: some source values are not valid ` +
178
191
  `under the target field. Supply a finite business transform as the second argument.`,
@@ -247,12 +260,30 @@ function fieldEvolutionRules(
247
260
 
248
261
  /** Explicit identity projection: the tenant declares the business meaning is continuous;
249
262
  * the compiler still proves every old value is accepted and its physical decoding is unchanged. */
250
- function safeIdentityProjection(sourceShape: unknown, targetShape: unknown): boolean {
263
+ function safeIdentityProjection(
264
+ sourceShape: unknown,
265
+ targetShape: unknown,
266
+ identity?: { aggregateId: string; fieldName: string },
267
+ ): boolean {
251
268
  if (sourceShape === null || targetShape === null || typeof sourceShape !== "object" || typeof targetShape !== "object") {
252
269
  return canonicalJson(sourceShape) === canonicalJson(targetShape);
253
270
  }
254
271
  const source = sourceShape as Record<string, unknown>;
255
272
  const target = targetShape as Record<string, unknown>;
273
+ // RelationTuple.subjects is a kernel-native framework palette, not a tenant
274
+ // JSON field. Historical DSL metadata called its map leaf `json`, while the
275
+ // kernel already admitted/stored only RelationshipSubject values. The typed
276
+ // value-object declaration therefore discloses the existing contract without
277
+ // changing physical decoding or widening what old writers can author. Keep
278
+ // this proof bound to the one kernel-special field; arbitrary JSON->VO
279
+ // narrowing remains refused below.
280
+ if (
281
+ identity?.aggregateId === "RelationTuple" &&
282
+ identity.fieldName === "subjects" &&
283
+ isRelationshipSubjectDisclosure(source, target)
284
+ ) {
285
+ return true;
286
+ }
256
287
  const physical = (shape: Record<string, unknown>) => {
257
288
  const { kind, mapValueKind, jsonShape, encrypted } = shape;
258
289
  // `t.ref` and `t.foreignRef` store the target aggregate id as the same Str
@@ -267,6 +298,21 @@ function safeIdentityProjection(sourceShape: unknown, targetShape: unknown): boo
267
298
  && jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
268
299
  }
269
300
 
301
+ function isRelationshipSubjectDisclosure(
302
+ source: Record<string, unknown>,
303
+ target: Record<string, unknown>,
304
+ ): boolean {
305
+ const targetVo = target.mapValueVo as
306
+ | { descriptor?: { name?: unknown } }
307
+ | undefined;
308
+ return source.kind === "map" &&
309
+ target.kind === "map" &&
310
+ source.mapValueKind === "json" &&
311
+ target.mapValueKind === "json" &&
312
+ source.mapValueVo === undefined &&
313
+ targetVo?.descriptor?.name === "RelationshipSubject";
314
+ }
315
+
270
316
  /** Automatic widening is stricter: every non-schema business descriptor must remain
271
317
  * identical. Brands, units, references and null meaning require an explicit declaration. */
272
318
  function safeAutomaticWidening(sourceShape: unknown, targetShape: unknown): boolean {
@@ -310,7 +356,21 @@ function jsonSchemaSubset(source: unknown, target: unknown): boolean {
310
356
  return false;
311
357
  }
312
358
  }
313
- if (s.additionalProperties !== false && t.additionalProperties === false) return false;
359
+ const sourceAdditional = s.additionalProperties;
360
+ const targetAdditional = t.additionalProperties;
361
+ if (sourceAdditional !== false) {
362
+ if (targetAdditional === false) return false;
363
+ if (targetAdditional !== undefined && targetAdditional !== true) {
364
+ if (
365
+ sourceAdditional === undefined ||
366
+ sourceAdditional === true ||
367
+ typeof sourceAdditional !== "object" ||
368
+ !jsonSchemaSubset(sourceAdditional, targetAdditional)
369
+ ) {
370
+ return false;
371
+ }
372
+ }
373
+ }
314
374
  }
315
375
  if (sourceTypes.has("array") && s.items !== undefined && t.items !== undefined) {
316
376
  if (!jsonSchemaSubset(s.items, t.items)) return false;