@githolon/dsl 0.42.0 → 0.43.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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.42.0",
3
+ "version": "0.43.0",
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",
package/src/aggregate.ts CHANGED
@@ -313,6 +313,14 @@ export function aggregate<const Id extends string, F extends Record<string, Fiel
313
313
  ? { hasInvariant: true as const, invariant: composedInvariant }
314
314
  : {}),
315
315
  ...(fromBirthFields.length > 0 ? { fromBirthFields } : {}),
316
+ // VALUE-FREE INVARIANT (nomosFlowGate rule 4a): when the ONLY invariant is the synthesized
317
+ // fromBirth presence check, its refusal correlates with STRUCTURE the law itself guarantees
318
+ // publicly (a fromBirth field is present on every lawful row) — never with a secret VALUE.
319
+ // The flow gate exempts it from the declassify requirement; ANY user invariant (which may
320
+ // read plaintext values) re-arms the rule.
321
+ ...(fromBirthFields.length > 0 && userInvariant === undefined
322
+ ? { invariantValueFree: true as const }
323
+ : {}),
316
324
  ...(opts?.invariantDeclassify !== undefined ? { invariantDeclassify: opts.invariantDeclassify } : {}),
317
325
  ...(opts?.cap !== undefined ? { cap: opts.cap } : {}),
318
326
  ...(opts?.global === true ? { global: true } : {}),
@@ -1392,6 +1392,10 @@ function voToJsonExpr(f: ValueObjectFieldDescriptor, expr: string, depth = 0): s
1392
1392
  * per file when the file emits ≥1 VO class (`toJson()`-based value equality). */
1393
1393
  function emitVoHelpers(): string {
1394
1394
  return [
1395
+ `/// The copyWith ABSENT-vs-null sentinel: an omitted optional param keeps the current`,
1396
+ `/// value; an explicit \`null\` clears it. (Dart cannot distinguish the two otherwise.)`,
1397
+ `const Object _voUnset = Object();`,
1398
+ ``,
1395
1399
  `/// Deep json equality for generated value-object classes (toJson-based).`,
1396
1400
  `bool _voJsonEq(Object? a, Object? b) {`,
1397
1401
  ` if (a is Map && b is Map) {`,
@@ -1428,6 +1432,36 @@ function emitVoHelpers(): string {
1428
1432
  * `toJson` (the authored payload shape — optionals OMITTED when null, matching the zod
1429
1433
  * `.optional()` contract), and toJson-based value equality. A `geo.*` VO emits the
1430
1434
  * fixed RFC 7946 shape (typed `coordinates`; `type` is the constant geometry kind). */
1435
+ /** The generated `copyWith` for a VO/arm class: non-nullable fields take an optional param
1436
+ * (`?? this.x` — they cannot be cleared; presence is the type's contract); OPTIONAL fields use
1437
+ * the `_voUnset` sentinel so `copyWith()` keeps the value while `copyWith(x: null)` CLEARS it —
1438
+ * the two intents Dart's named params cannot otherwise distinguish. The Dart idiom for the TS
1439
+ * spread (dart_codegen_parity.md P3). */
1440
+ function emitCopyWith(
1441
+ cls: string,
1442
+ fields: readonly { member: string; dartType: string; optional: boolean }[],
1443
+ ): string[] {
1444
+ if (fields.length === 0) return [];
1445
+ const params = fields.map((x) =>
1446
+ x.optional ? ` Object? ${x.member} = _voUnset,` : ` ${x.dartType}? ${x.member},`,
1447
+ );
1448
+ const args = fields.map((x) =>
1449
+ x.optional
1450
+ ? ` ${x.member}: identical(${x.member}, _voUnset) ? this.${x.member} : ${x.member} as ${x.dartType}?,`
1451
+ : ` ${x.member}: ${x.member} ?? this.${x.member},`,
1452
+ );
1453
+ return [
1454
+ ` /// A copy with the given fields replaced. Omitting an optional param keeps its value;`,
1455
+ ` /// passing \`null\` explicitly CLEARS it (the _voUnset sentinel distinguishes the two).`,
1456
+ ` ${cls} copyWith({`,
1457
+ ...params,
1458
+ ` }) =>`,
1459
+ ` ${cls}(`,
1460
+ ...args,
1461
+ ` );`,
1462
+ ];
1463
+ }
1464
+
1431
1465
  /** Emit a `union(...)` as a SEALED Dart hierarchy: the sealed base (decode switch on the
1432
1466
  * blessed `type` discriminator + abstract `toJson`), one final arm class per arm (anonymous
1433
1467
  * arms carry their fields; NAMED-VO arms hold a single `value` of the named class), and the
@@ -1464,6 +1498,9 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
1464
1498
  ` ${cls}(${arm.ref}.fromProjected((m['value']! as Map).cast<String, Object?>()));`,
1465
1499
  ` @override`,
1466
1500
  ` Map<String, Object?> toJson() => <String, Object?>{'type': '${key}', 'value': value.toJson()};`,
1501
+ ``,
1502
+ ` /// A copy with [value] replaced.`,
1503
+ ` ${cls} copyWith({${arm.ref}? value}) => ${cls}(value ?? this.value);`,
1467
1504
  ` @override`,
1468
1505
  ` bool operator ==(Object other) => other is ${cls} && _voJsonEq(other.toJson(), toJson());`,
1469
1506
  ` @override`,
@@ -1512,6 +1549,8 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
1512
1549
  ...literalToLines,
1513
1550
  ...toLines,
1514
1551
  ` };`,
1552
+ ``,
1553
+ ...emitCopyWith(cls, fields.map((x) => ({ member: x.member, dartType: dartTypeOfVoField(x.f, cls, x.wireName), optional: x.optional }))),
1515
1554
  ` @override`,
1516
1555
  ` bool operator ==(Object other) => other is ${cls} && _voJsonEq(other.toJson(), toJson());`,
1517
1556
  ` @override`,
@@ -1624,6 +1663,8 @@ function emitValueObjectClass(d: ValueObjectDescriptor): string {
1624
1663
  ` 'coordinates': coordinates,`,
1625
1664
  ` };`,
1626
1665
  ``,
1666
+ ...emitCopyWith(d.name, [{ member: "coordinates", dartType: coordsType, optional: false }]),
1667
+ ``,
1627
1668
  ` @override`,
1628
1669
  ` bool operator ==(Object other) =>`,
1629
1670
  ` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
@@ -1686,6 +1727,8 @@ function emitValueObjectClass(d: ValueObjectDescriptor): string {
1686
1727
  ...toLines,
1687
1728
  ` };`,
1688
1729
  ``,
1730
+ ...emitCopyWith(d.name, fields.map((x) => ({ member: x.member, dartType: dartTypeOfVoField(x.f, d.name, x.wireName), optional: x.optional }))),
1731
+ ``,
1689
1732
  ` @override`,
1690
1733
  ` bool operator ==(Object other) =>`,
1691
1734
  ` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
package/src/flow_gate.ts CHANGED
@@ -38,6 +38,10 @@ type FlowModule = {
38
38
  readonly aggregates?: readonly {
39
39
  readonly id: string;
40
40
  readonly hasInvariant?: boolean;
41
+ /** True when the invariant is PROVABLY value-free (only the synthesized fromBirth
42
+ * presence check — no user predicate). Its refusal correlates with law-guaranteed
43
+ * structure, never a secret value, so rule 4a exempts it from declassify. */
44
+ readonly invariantValueFree?: boolean;
41
45
  readonly invariantDeclassify?: string;
42
46
  readonly encryptedFields?: readonly string[];
43
47
  readonly fields?: Record<string, unknown>;
@@ -150,6 +154,9 @@ export function assertFlowGate(m: CanonicalManifest, mod?: FlowModule): void {
150
154
  // 4a — an aggregate `invariant` over a private aggregate with a plaintext field.
151
155
  for (const a of mod.aggregates ?? []) {
152
156
  if (!a.hasInvariant || !privateTypes.has(a.id) || a.invariantDeclassify) continue;
157
+ // A VALUE-FREE invariant (the synthesized fromBirth presence check alone) refuses only on
158
+ // ABSENCE of a field the law guarantees present — structure, not secret. No declassify needed.
159
+ if (a.invariantValueFree === true) continue;
153
160
  const enc = new Set(a.encryptedFields ?? []);
154
161
  const hasPlaintext = Object.keys(a.fields ?? {}).some((f) => !enc.has(f));
155
162
  if (!hasPlaintext) continue; // fully-encrypted: the invariant reads only ciphertext — can't leak this way