@githolon/dsl 0.54.1 → 0.55.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/dart/lib/nomos_client.dart +3 -0
- package/dart/lib/src/null_slot.dart +128 -0
- package/dart/test/null_slot_test.dart +51 -0
- package/package.json +1 -1
- package/src/authoring.ts +19 -0
- package/src/build_package.ts +5 -0
- package/src/clear_directive.ts +103 -0
- package/src/codegen_dart.ts +370 -30
- package/src/codegen_openapi.ts +23 -4
- package/src/codegen_proof.ts +73 -5
- package/src/codegen_ts.ts +150 -12
- package/src/compile_package_main.ts +20 -1
- package/src/fields.ts +221 -5
- package/src/index.ts +10 -0
- package/src/law_check.ts +183 -1
- package/src/null_meta.ts +65 -0
- package/src/ops.ts +32 -0
- package/src/wire_encode.ts +42 -2
- package/src/zod_lazy.ts +45 -0
package/src/wire_encode.ts
CHANGED
|
@@ -31,7 +31,7 @@ import {
|
|
|
31
31
|
planBirthRecordOps,
|
|
32
32
|
} from "./birth_meter.js";
|
|
33
33
|
import type { AggregateHandle } from "./aggregate.js";
|
|
34
|
-
import type
|
|
34
|
+
import { admitsNull, type Field, type EvidenceRefValue } from "./fields.js";
|
|
35
35
|
import type { Directive, ReferentialMarker } from "./directive.js";
|
|
36
36
|
import type { PlannedOp, FieldOp } from "./ops.js";
|
|
37
37
|
import { deterministicPorts, type Ports } from "./ctx.js";
|
|
@@ -77,6 +77,15 @@ export function encodeKernelSchema(agg: AggregateHandle): WireSchema {
|
|
|
77
77
|
return out;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/** Recursively true iff `value` is (or contains, at any depth) a json `null` — the no-null check the
|
|
81
|
+
* encode-path null gate runs on a forbid `t.json()` field. Bounded by the value's own structure. */
|
|
82
|
+
function jsonHasNull(value: unknown): boolean {
|
|
83
|
+
if (value === null) return true;
|
|
84
|
+
if (Array.isArray(value)) return value.some(jsonHasNull);
|
|
85
|
+
if (typeof value === "object") return Object.values(value as Record<string, unknown>).some(jsonHasNull);
|
|
86
|
+
return false;
|
|
87
|
+
}
|
|
88
|
+
|
|
80
89
|
/** A shape description for a value that failed to encode (diagnostics only). */
|
|
81
90
|
function describeShape(value: unknown): string {
|
|
82
91
|
if (value === null) return "null";
|
|
@@ -93,6 +102,21 @@ function encodeKernelValue(field: Field | undefined, value: unknown, op?: FieldO
|
|
|
93
102
|
// string): the authored {hash, mediaType, byteLength, storageClass} maps to the
|
|
94
103
|
// kernel's snake_case serde shape + the `Git`/`External` unit-variant. Checked BEFORE
|
|
95
104
|
// the scalar fallthroughs (an evidence value is an object, never a Str/Int).
|
|
105
|
+
// NULL SEMANTICS (`architecture/null_semantics.md`) — the LUMP null gate, top-level null. A null
|
|
106
|
+
// value is lawful ONLY on a field that declared a null-admitting semantic (nullable / explicit-empty
|
|
107
|
+
// / clearable via `clear(...)`); it lowers to `Value::Json(null)` regardless of the field's kind (a
|
|
108
|
+
// cleared string clears to json null). A null on a forbid field is a typed refusal HERE (the encode
|
|
109
|
+
// runs in the sealed engine on every peer) — the runtime twin of the check-time refusal. Era-safe: a
|
|
110
|
+
// pre-null-semantics package froze its old lump (no `nullSemantic`), so this only tightens new law.
|
|
111
|
+
if (value === null && field !== undefined) {
|
|
112
|
+
if (admitsNull(field)) return { Json: null };
|
|
113
|
+
const aggName = op?.aggregateType ?? op?.aggregateId ?? "?";
|
|
114
|
+
throw new Error(
|
|
115
|
+
`${aggName}.${op?.field ?? field.kind}: writes null, but null has no law-level meaning here. ` +
|
|
116
|
+
`Use t.clearable(...) + clear(...) for explicit clearing, or t.nullable(..., { reason }) / ` +
|
|
117
|
+
`t.json({ null: "explicit-empty", reason }) for a real empty value.`,
|
|
118
|
+
);
|
|
119
|
+
}
|
|
96
120
|
if (field?.kind === "evidence") {
|
|
97
121
|
const v = value as EvidenceRefValue;
|
|
98
122
|
return {
|
|
@@ -109,7 +133,23 @@ function encodeKernelValue(field: Field | undefined, value: unknown, op?: FieldO
|
|
|
109
133
|
// value — object, array, string, number, boolean, null — commits as one consistent value kind
|
|
110
134
|
// (the kernel canonicalises it at the boundary). This is the new-era encoding; a package compiled
|
|
111
135
|
// before this change froze the old `{ Str: JSON.stringify(value) }` lump and replays unchanged.
|
|
112
|
-
if (field?.kind === "json")
|
|
136
|
+
if (field?.kind === "json") {
|
|
137
|
+
// NULL SEMANTICS (`architecture/null_semantics.md`) — the LUMP null gate. A `t.json()` field forbids
|
|
138
|
+
// null at every depth unless it declared a null-admitting semantic (nullable / explicit-empty /
|
|
139
|
+
// clearable via `clear(...)`). A plan-injected null on a forbid field is a typed refusal HERE (the
|
|
140
|
+
// encode runs inside the sealed engine on every peer), the runtime twin of the check-time refusal.
|
|
141
|
+
// Era-safe: a pre-null-semantics package froze its old lump and never carries a `nullSemantic`, so
|
|
142
|
+
// this branch only tightens NEWLY compiled law (the field defaults to "forbid" at construction).
|
|
143
|
+
if (!admitsNull(field) && jsonHasNull(value)) {
|
|
144
|
+
const aggName = op?.aggregateType ?? op?.aggregateId ?? "?";
|
|
145
|
+
throw new Error(
|
|
146
|
+
`${aggName}.${op?.field ?? field.kind}: json field carries a null, but null has no law-level ` +
|
|
147
|
+
`meaning here. Use t.clearable(...) + clear(...) for explicit clearing, or ` +
|
|
148
|
+
`t.nullable(..., { reason }) / t.json({ null: "explicit-empty", reason }) for a real empty value.`,
|
|
149
|
+
);
|
|
150
|
+
}
|
|
151
|
+
return { Json: value };
|
|
152
|
+
}
|
|
113
153
|
// A `t.map(t.json())` / `t.map(VO.field())` ENTRY value is a json leaf too. Its wire DRIVER is
|
|
114
154
|
// `MapOf(Lww)` (whose leaves are ordinary kernel `Value`s), so a json-valued entry lowers to the SAME
|
|
115
155
|
// first-class `Value::Json` a top-level json field does — the map entry round-trips as its OBJECT, not a
|
package/src/zod_lazy.ts
ADDED
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Dependency-free helpers for RECURSIVE `z.lazy` payload schemas, shared by BOTH codegen walkers
|
|
3
|
+
* (codegen_ts + codegen_dart) WITHOUT coupling them. Keeping these here (rather than in codegen_ts)
|
|
4
|
+
* avoids a codegen_ts↔codegen_dart import cycle that would otherwise drag codegen_ts's heavy
|
|
5
|
+
* transitive deps (workspace_routing → node:crypto) into the tenant ENGINE bundle (esbuild
|
|
6
|
+
* neutral-platform, no node builtins). This module imports NOTHING but zod's type.
|
|
7
|
+
*/
|
|
8
|
+
import type { z } from "zod";
|
|
9
|
+
|
|
10
|
+
function lazyDef(zt: z.ZodTypeAny): { type?: unknown; getter?: () => z.ZodTypeAny } {
|
|
11
|
+
const raw = zt as unknown as { _def?: { type?: unknown; getter?: () => z.ZodTypeAny }; def?: { type?: unknown; getter?: () => z.ZodTypeAny } };
|
|
12
|
+
return raw._def ?? raw.def ?? {};
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Resolve a `z.lazy(() => …)` ONE level to its inner schema (zod v4 exposes the deferred getter on
|
|
17
|
+
* `_def.getter`). A non-lazy schema is returned unchanged. Recursive `z.lazy` schemas (a `TreeNode`
|
|
18
|
+
* that references itself) are common for tree/JSON payloads; every schema WALKER unwraps here and
|
|
19
|
+
* carries a VISITED / registry keyed on the lazy object's IDENTITY so a self-reference is a
|
|
20
|
+
* detectable knot, never an infinite descent.
|
|
21
|
+
*/
|
|
22
|
+
export function zodUnwrapLazy(zt: z.ZodTypeAny): z.ZodTypeAny {
|
|
23
|
+
const def = lazyDef(zt);
|
|
24
|
+
if (def.type === "lazy" && typeof def.getter === "function") return def.getter();
|
|
25
|
+
return zt;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const capFirst = (s: string): string => (s.length === 0 ? s : s[0]!.toUpperCase() + s.slice(1));
|
|
29
|
+
const camelize = (s: string): string => s.replace(/[_\s-]+(\w)/g, (_m, c: string) => c.toUpperCase());
|
|
30
|
+
|
|
31
|
+
/**
|
|
32
|
+
* A PascalCase type name from a payload path (`saveTree.root` → `SaveTreeRoot`), array markers
|
|
33
|
+
* dropped. The SAME base the TS and Dart recursive-type synthesizers use so a recursive type's
|
|
34
|
+
* TS alias and Dart class agree in the common case.
|
|
35
|
+
*/
|
|
36
|
+
export function recursiveTypeBaseName(path: string): string {
|
|
37
|
+
return (
|
|
38
|
+
path
|
|
39
|
+
.replace(/\[\]/g, "")
|
|
40
|
+
.split(/[.]/)
|
|
41
|
+
.filter(Boolean)
|
|
42
|
+
.map((s) => capFirst(camelize(s)))
|
|
43
|
+
.join("") || "Recursive"
|
|
44
|
+
);
|
|
45
|
+
}
|