@githolon/dsl 0.42.0 → 0.44.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 +1 -1
- package/src/aggregate.ts +8 -0
- package/src/codegen_dart.ts +45 -0
- package/src/flow_gate.ts +20 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.44.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 } : {}),
|
package/src/codegen_dart.ts
CHANGED
|
@@ -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
|
|
@@ -1441,6 +1475,8 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
|
|
|
1441
1475
|
`/// Union \`${d.name}\` — a sum type stored as ONE json leaf (wholesale-replace; the`,
|
|
1442
1476
|
`/// \`type\` discriminator names the arm). Adding an arm is ADDITIVE (older clients decode`,
|
|
1443
1477
|
`/// it as [${d.name}Unknown]); removing/renaming an arm is a retype (#58 dispositions).`,
|
|
1478
|
+
`/// \`.encrypted()\` on a field of this type seals the WHOLE leaf — discriminator included;`,
|
|
1479
|
+
`/// per-arm sensitivity cannot be split (model differently-classified arms as separate fields).`,
|
|
1444
1480
|
`sealed class ${d.name} {`,
|
|
1445
1481
|
` const ${d.name}();`,
|
|
1446
1482
|
` Map<String, Object?> toJson();`,
|
|
@@ -1464,6 +1500,9 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
|
|
|
1464
1500
|
` ${cls}(${arm.ref}.fromProjected((m['value']! as Map).cast<String, Object?>()));`,
|
|
1465
1501
|
` @override`,
|
|
1466
1502
|
` Map<String, Object?> toJson() => <String, Object?>{'type': '${key}', 'value': value.toJson()};`,
|
|
1503
|
+
``,
|
|
1504
|
+
` /// A copy with [value] replaced.`,
|
|
1505
|
+
` ${cls} copyWith({${arm.ref}? value}) => ${cls}(value ?? this.value);`,
|
|
1467
1506
|
` @override`,
|
|
1468
1507
|
` bool operator ==(Object other) => other is ${cls} && _voJsonEq(other.toJson(), toJson());`,
|
|
1469
1508
|
` @override`,
|
|
@@ -1512,6 +1551,8 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
|
|
|
1512
1551
|
...literalToLines,
|
|
1513
1552
|
...toLines,
|
|
1514
1553
|
` };`,
|
|
1554
|
+
``,
|
|
1555
|
+
...emitCopyWith(cls, fields.map((x) => ({ member: x.member, dartType: dartTypeOfVoField(x.f, cls, x.wireName), optional: x.optional }))),
|
|
1515
1556
|
` @override`,
|
|
1516
1557
|
` bool operator ==(Object other) => other is ${cls} && _voJsonEq(other.toJson(), toJson());`,
|
|
1517
1558
|
` @override`,
|
|
@@ -1624,6 +1665,8 @@ function emitValueObjectClass(d: ValueObjectDescriptor): string {
|
|
|
1624
1665
|
` 'coordinates': coordinates,`,
|
|
1625
1666
|
` };`,
|
|
1626
1667
|
``,
|
|
1668
|
+
...emitCopyWith(d.name, [{ member: "coordinates", dartType: coordsType, optional: false }]),
|
|
1669
|
+
``,
|
|
1627
1670
|
` @override`,
|
|
1628
1671
|
` bool operator ==(Object other) =>`,
|
|
1629
1672
|
` other is ${d.name} && _voJsonEq(other.toJson(), toJson());`,
|
|
@@ -1686,6 +1729,8 @@ function emitValueObjectClass(d: ValueObjectDescriptor): string {
|
|
|
1686
1729
|
...toLines,
|
|
1687
1730
|
` };`,
|
|
1688
1731
|
``,
|
|
1732
|
+
...emitCopyWith(d.name, fields.map((x) => ({ member: x.member, dartType: dartTypeOfVoField(x.f, d.name, x.wireName), optional: x.optional }))),
|
|
1733
|
+
``,
|
|
1689
1734
|
` @override`,
|
|
1690
1735
|
` bool operator ==(Object other) =>`,
|
|
1691
1736
|
` 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
|
|
@@ -166,8 +173,19 @@ export function assertFlowGate(m: CanonicalManifest, mod?: FlowModule): void {
|
|
|
166
173
|
let refs: readonly { readonly aggregate: string }[];
|
|
167
174
|
try {
|
|
168
175
|
refs = wi.reads({ intent: new Proxy({}, { get: () => "" }) as Record<string, unknown>, principal: "" }) ?? [];
|
|
169
|
-
} catch {
|
|
170
|
-
|
|
176
|
+
} catch (e) {
|
|
177
|
+
// THE 4b PROBE HOLE, CLOSED (classified_first_class.md §2.1): a read-set that cannot be
|
|
178
|
+
// resolved on the probe used to be SKIPPED — "conservative on availability" was a hole on
|
|
179
|
+
// SAFETY (an invariant could escape rule 4 by accident of a throwing reads()). On a domain
|
|
180
|
+
// that HAS private types, an unresolvable read-set now REFUSES: make reads() total over a
|
|
181
|
+
// probe intent (every field reads as ""), or declare declassify if the read-set is
|
|
182
|
+
// genuinely intent-dependent and touches private state.
|
|
183
|
+
throw new FlowGateError(
|
|
184
|
+
`[nomosFlowGate rule 4b — unresolvable read-set] workspaceInvariant '${wi.id}' on '${wi.on}': its ` +
|
|
185
|
+
`.reads() threw on the compile probe (${String((e as Error)?.message ?? e).slice(0, 120)}) so the gate ` +
|
|
186
|
+
`cannot prove the refusal is secret-free while this domain declares private types. Make .reads() total ` +
|
|
187
|
+
`over a probe intent (every intent field reads as ""), or acknowledge with .declassify("<why>").`,
|
|
188
|
+
);
|
|
171
189
|
}
|
|
172
190
|
for (const r of refs) {
|
|
173
191
|
const short = r.aggregate.includes(".") ? r.aggregate.slice(r.aggregate.lastIndexOf(".") + 1) : r.aggregate;
|