@githolon/dsl 0.45.0 → 0.46.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/CHANGELOG.md +5 -0
- package/dart/lib/src/webview_bridge.dart +38 -4
- package/dart/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/src/codegen_dart.ts +25 -14
- package/src/codegen_dart_client.ts +18 -1
- package/src/codegen_proof.ts +132 -15
- package/src/codegen_ts.ts +8 -0
- package/src/value_object.ts +10 -9
package/dart/CHANGELOG.md
CHANGED
|
@@ -1,3 +1,8 @@
|
|
|
1
|
+
## 0.32.0
|
|
2
|
+
|
|
3
|
+
- SPATIAL READS: `NomosBbox` + `NomosBridge.spatialWithin(spatialId, bbox)` + `watchSpatial(...)` — the R*Tree probe surfaces on the bridge; generated clients (tooling ≥0.46) emit typed `sitesByBounds({required NomosBbox bbox})` + watch accessors.
|
|
4
|
+
- Analyzer-clean under strict lints (curly-braces rule) — the two remaining unbraced ifs in the bridge are gone.
|
|
5
|
+
|
|
1
6
|
## 0.31.0
|
|
2
7
|
|
|
3
8
|
- `NomosFieldMeta.fromBirth` — the registry surfaces BIRTH-PRESENCE LAW (`.fromBirth()` fields, tooling ≥0.42): a gate invariant guarantees the field present on every row, and the generated read models type it non-nullable.
|
|
@@ -275,6 +275,23 @@ class NomosCreated {
|
|
|
275
275
|
String toString() => id;
|
|
276
276
|
}
|
|
277
277
|
|
|
278
|
+
/// A geographic bounding box for spatial reads (`[minLng, minLat] .. [maxLng, maxLat]`,
|
|
279
|
+
/// RFC 7946 lng/lat order — the R*Tree probe's extent).
|
|
280
|
+
class NomosBbox {
|
|
281
|
+
final double minLng;
|
|
282
|
+
final double minLat;
|
|
283
|
+
final double maxLng;
|
|
284
|
+
final double maxLat;
|
|
285
|
+
const NomosBbox({
|
|
286
|
+
required this.minLng,
|
|
287
|
+
required this.minLat,
|
|
288
|
+
required this.maxLng,
|
|
289
|
+
required this.maxLat,
|
|
290
|
+
});
|
|
291
|
+
Map<String, Object?> toJson() =>
|
|
292
|
+
{'minLng': minLng, 'minLat': minLat, 'maxLng': maxLng, 'maxLat': maxLat};
|
|
293
|
+
}
|
|
294
|
+
|
|
278
295
|
/// The result of [NomosBridge.offerCreates]: the new local [head] plus every aggregate the plan minted.
|
|
279
296
|
class NomosOfferOutcome {
|
|
280
297
|
final String head;
|
|
@@ -341,8 +358,9 @@ class NomosBridge {
|
|
|
341
358
|
Future<void> get ready => _ready.future;
|
|
342
359
|
|
|
343
360
|
void _onMessage(Map<String, Object?> m) {
|
|
344
|
-
if (m['v'] != kWebviewBridgeVersion)
|
|
361
|
+
if (m['v'] != kWebviewBridgeVersion) {
|
|
345
362
|
return; // a foreign-version message is ignored (forward-compatible)
|
|
363
|
+
}
|
|
346
364
|
final event = m['event'];
|
|
347
365
|
if (event == 'ready') {
|
|
348
366
|
if (!_ready.isCompleted) _ready.complete();
|
|
@@ -411,8 +429,9 @@ class NomosBridge {
|
|
|
411
429
|
if (event == 'span') {
|
|
412
430
|
// LIVE per-layer span: { event:'span', span:{ phase:'begin'|'end', id, track, name, kind, t0|t1 } }.
|
|
413
431
|
_spanBuffer.add(m);
|
|
414
|
-
if (_spanBuffer.length > 400)
|
|
432
|
+
if (_spanBuffer.length > 400) {
|
|
415
433
|
_spanBuffer.removeRange(0, _spanBuffer.length - 400);
|
|
434
|
+
}
|
|
416
435
|
if (!_spanEvents.isClosed) _spanEvents.add(m);
|
|
417
436
|
return;
|
|
418
437
|
}
|
|
@@ -761,6 +780,13 @@ class NomosBridge {
|
|
|
761
780
|
Future<List<Map<String, dynamic>>> queryById(String id) async =>
|
|
762
781
|
_asRows(await _call('queryById', {'id': id}));
|
|
763
782
|
|
|
783
|
+
/// THE SPATIAL READ: rows of the declared spatial index's of-type whose geometry
|
|
784
|
+
/// intersects [bbox] — an R*Tree probe on the local projection, same row shape as [query].
|
|
785
|
+
Future<List<Map<String, dynamic>>> spatialWithin(
|
|
786
|
+
String spatialId, NomosBbox bbox) async =>
|
|
787
|
+
_asRows(await _call(
|
|
788
|
+
'spatialWithin', {'spatialId': spatialId, 'bbox': bbox.toJson()}));
|
|
789
|
+
|
|
764
790
|
/// A maintained O(1) count / sum.
|
|
765
791
|
Future<int> count(String countId, [String group = '']) async =>
|
|
766
792
|
((_asMap(await _call('count', {'countId': countId, 'group': group}))[
|
|
@@ -914,6 +940,10 @@ class NomosBridge {
|
|
|
914
940
|
Stream<List<dynamic>> watchById(String id) =>
|
|
915
941
|
_watchStream(kind: 'byId', id: id);
|
|
916
942
|
|
|
943
|
+
/// Reactive SPATIAL read: re-emits the R*Tree probe's rows whenever they change locally.
|
|
944
|
+
Stream<List<dynamic>> watchSpatial(String spatialId, NomosBbox bbox) =>
|
|
945
|
+
_watchStream(kind: 'spatial', spatialId: spatialId, bbox: bbox);
|
|
946
|
+
|
|
917
947
|
/// SHAPE COMPATIBILITY (#72): one-shot verdict for this client's structural [requirement]
|
|
918
948
|
/// (its baked `…InterfaceJson`, decoded) against the workspace's ON-LEDGER offer (auto-sourced).
|
|
919
949
|
/// `{ ok, missing[], incompatible[] }` — names the exact directive/aggregate/field, not a hash.
|
|
@@ -953,7 +983,9 @@ class NomosBridge {
|
|
|
953
983
|
{required String kind,
|
|
954
984
|
String? queryId,
|
|
955
985
|
Map<String, Object?>? params,
|
|
956
|
-
String? id
|
|
986
|
+
String? id,
|
|
987
|
+
String? spatialId,
|
|
988
|
+
NomosBbox? bbox}) {
|
|
957
989
|
final subId = 'w${++_seq}';
|
|
958
990
|
late final StreamController<List<dynamic>> ctrl;
|
|
959
991
|
ctrl = StreamController<List<dynamic>>(
|
|
@@ -962,7 +994,9 @@ class NomosBridge {
|
|
|
962
994
|
'kind': kind,
|
|
963
995
|
if (queryId != null) 'queryId': queryId,
|
|
964
996
|
if (params != null) 'params': params,
|
|
965
|
-
if (id != null) 'id': id
|
|
997
|
+
if (id != null) 'id': id,
|
|
998
|
+
if (spatialId != null) 'spatialId': spatialId,
|
|
999
|
+
if (bbox != null) 'bbox': bbox.toJson(),
|
|
966
1000
|
}),
|
|
967
1001
|
onCancel: () async {
|
|
968
1002
|
_watches.remove(subId);
|
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.32.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.46.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/codegen_dart.ts
CHANGED
|
@@ -810,19 +810,21 @@ function collectEnums(
|
|
|
810
810
|
// hand-maintained `hide` list on the package barrel. The prefix is the field's
|
|
811
811
|
// semantic home, not an accident of import order.
|
|
812
812
|
const domainPrefix = dartTypeNameFromField(domainKey);
|
|
813
|
-
|
|
813
|
+
// SEMANTIC ENUM NAMES (co2 feedback): the type is named by its SEMANTIC HOME —
|
|
814
|
+
// `<Domain><Owner><Field>` (`EstateSiteStatus`, not `EstateStatus5Enum`). The owner is the
|
|
815
|
+
// aggregate (or the payload's target aggregate / directive) the field lives on. A value-set
|
|
816
|
+
// shared across owners keeps the FIRST owner's name (one identical type, aggregates register
|
|
817
|
+
// before payloads so the aggregate's name wins). Numeric suffixes survive only as the
|
|
818
|
+
// last-resort collision guard — with owner-qualified bases they should never fire.
|
|
819
|
+
const register = (preferredName: string, values: readonly string[], owner = "") => {
|
|
814
820
|
const key = values.join("|");
|
|
815
821
|
if (dartTypes.has(key)) return;
|
|
816
|
-
|
|
817
|
-
// several `status` fields with distinct literal lists all want `StatusEnum`).
|
|
818
|
-
// Suffix on collision so each distinct value-set gets a UNIQUE Dart enum
|
|
819
|
-
// (`EstateStatusEnum`, `EstateStatus2Enum`).
|
|
820
|
-
const base = `${domainPrefix}${dartTypeNameFromField(preferredName)}Enum`;
|
|
822
|
+
const base = `${domainPrefix}${dartTypeNameFromField(owner)}${dartTypeNameFromField(preferredName)}`;
|
|
821
823
|
let name = base;
|
|
822
824
|
let n = 1;
|
|
823
825
|
while (usedNames.has(name)) {
|
|
824
826
|
n += 1;
|
|
825
|
-
name = `${
|
|
827
|
+
name = `${base}${n}`;
|
|
826
828
|
}
|
|
827
829
|
usedNames.add(name);
|
|
828
830
|
dartTypes.set(key, name);
|
|
@@ -846,7 +848,7 @@ function collectEnums(
|
|
|
846
848
|
)) {
|
|
847
849
|
if (field.kind === "enum") {
|
|
848
850
|
// Unwrap `.optional()` so an optional enum field still registers its options.
|
|
849
|
-
register(name, zodEnumValues(unwrapZod(field.zod)));
|
|
851
|
+
register(name, zodEnumValues(unwrapZod(field.zod)), agg.id);
|
|
850
852
|
}
|
|
851
853
|
}
|
|
852
854
|
}
|
|
@@ -856,7 +858,7 @@ function collectEnums(
|
|
|
856
858
|
for (const [name, zt] of Object.entries(shape)) {
|
|
857
859
|
const inner = unwrapZod(zt as z.ZodTypeAny);
|
|
858
860
|
if (zodTypeName(inner) === "ZodEnum") {
|
|
859
|
-
register(name, zodEnumValues(inner));
|
|
861
|
+
register(name, zodEnumValues(inner), (d as { aggregateId?: string }).aggregateId ?? d.id);
|
|
860
862
|
}
|
|
861
863
|
}
|
|
862
864
|
}
|
|
@@ -1246,7 +1248,8 @@ const geometryCoordsDart: Record<string, string> = {
|
|
|
1246
1248
|
* VOs stay separate types (VO names are package-unique, so so are these).
|
|
1247
1249
|
*/
|
|
1248
1250
|
function voEnumName(voName: string, fieldName: string): string {
|
|
1249
|
-
|
|
1251
|
+
// SEMANTIC NAME (co2 feedback): `<VoName><Field>` — `SiteLocationSource`, not `…SourceEnum`.
|
|
1252
|
+
return `${voName}${dartTypeNameFromField(fieldName)}`;
|
|
1250
1253
|
}
|
|
1251
1254
|
|
|
1252
1255
|
/** The tolerant-decode SENTINEL case name — dodge a literal enum value that would
|
|
@@ -1399,16 +1402,24 @@ function emitVoHelpers(): string {
|
|
|
1399
1402
|
`/// Deep json equality for generated value-object classes (toJson-based).`,
|
|
1400
1403
|
`bool _voJsonEq(Object? a, Object? b) {`,
|
|
1401
1404
|
` if (a is Map && b is Map) {`,
|
|
1402
|
-
` if (a.length != b.length)
|
|
1405
|
+
` if (a.length != b.length) {`,
|
|
1406
|
+
` return false;`,
|
|
1407
|
+
` }`,
|
|
1403
1408
|
` for (final k in a.keys) {`,
|
|
1404
|
-
` if (!b.containsKey(k) || !_voJsonEq(a[k], b[k]))
|
|
1409
|
+
` if (!b.containsKey(k) || !_voJsonEq(a[k], b[k])) {`,
|
|
1410
|
+
` return false;`,
|
|
1411
|
+
` }`,
|
|
1405
1412
|
` }`,
|
|
1406
1413
|
` return true;`,
|
|
1407
1414
|
` }`,
|
|
1408
1415
|
` if (a is List && b is List) {`,
|
|
1409
|
-
` if (a.length != b.length)
|
|
1416
|
+
` if (a.length != b.length) {`,
|
|
1417
|
+
` return false;`,
|
|
1418
|
+
` }`,
|
|
1410
1419
|
` for (var i = 0; i < a.length; i++) {`,
|
|
1411
|
-
` if (!_voJsonEq(a[i], b[i]))
|
|
1420
|
+
` if (!_voJsonEq(a[i], b[i])) {`,
|
|
1421
|
+
` return false;`,
|
|
1422
|
+
` }`,
|
|
1412
1423
|
` }`,
|
|
1413
1424
|
` return true;`,
|
|
1414
1425
|
` }`,
|
|
@@ -137,7 +137,7 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
137
137
|
// appear — only the actor identity (the bridge owns signing).
|
|
138
138
|
const gate = (d as { requiresRelationGate?: { relation: string } }).requiresRelationGate;
|
|
139
139
|
const gateGuard = gate !== undefined
|
|
140
|
-
? ` if (actor == null || actor!.trim().isEmpty) throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: ${dartStr(gate.relation)})
|
|
140
|
+
? ` if (actor == null || actor!.trim().isEmpty) {\n throw MissingOfferAuthority(domain: ${dartStr(domain)}, directiveId: ${dartStr(d.id)}, requires: ${dartStr(gate.relation)});\n }`
|
|
141
141
|
: "";
|
|
142
142
|
const gateDoc = gate !== undefined
|
|
143
143
|
? ` /// GATED: needs an \`actor\` (the \`${gate.relation}\` relation) — throws MissingOfferAuthority otherwise.`
|
|
@@ -218,6 +218,23 @@ export function generateDartBridgeClient(mod: DomainModule, opts: DartBridgeClie
|
|
|
218
218
|
);
|
|
219
219
|
}
|
|
220
220
|
|
|
221
|
+
// ── declared spatial indexes → typed bbox reads (lane-2 read-serving) ──
|
|
222
|
+
for (const sp of (mod.spatials ?? []) as { id: string; of: string; on: string }[]) {
|
|
223
|
+
const model = readModelClassName(sp.of);
|
|
224
|
+
const m = safeMethod(lcFirst(camel(sp.id)));
|
|
225
|
+
methods.push(
|
|
226
|
+
` /// Declared spatial index \`${sp.id}\` — rows of \`${sp.of}\` whose \`${sp.on}\` geometry`,
|
|
227
|
+
` /// intersects [bbox] (an R*Tree probe on the local projection, never a scan).`,
|
|
228
|
+
` Future<List<${model}>> ${m}({required NomosBbox bbox}) async =>`,
|
|
229
|
+
` (await bridge.spatialWithin(${dartStr(sp.id)}, bbox)).map((r) => ${rowToModel(model, "r")}).toList();`,
|
|
230
|
+
``,
|
|
231
|
+
` /// Reactive \`${sp.id}\` — re-emits when the probe's rows change locally.`,
|
|
232
|
+
` Stream<List<${model}>> watch${cap(camel(sp.id))}({required NomosBbox bbox}) =>`,
|
|
233
|
+
` bridge.watchSpatial(${dartStr(sp.id)}, bbox).map((rows) => rows.map((r) => ${rowToModel(model, "r")}).toList());`,
|
|
234
|
+
``,
|
|
235
|
+
);
|
|
236
|
+
}
|
|
237
|
+
|
|
221
238
|
// ── counts / sums → O(1) typed reads ──
|
|
222
239
|
for (const c of (mod.counts ?? []).map((raw) => finishCount(raw))) {
|
|
223
240
|
const m = safeMethod(lcFirst(camel(c.id)));
|
package/src/codegen_proof.ts
CHANGED
|
@@ -98,13 +98,13 @@ function sampleString(zt: z.ZodTypeAny, field: string): string {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/** One sample value for one zod shape. NO FALLBACK: an unhandled shape throws, naming its remedy. */
|
|
101
|
-
function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string): unknown {
|
|
101
|
+
function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string, voIncludeOptional = false): unknown {
|
|
102
102
|
// VALUE OBJECTS FIRST (vo_authoring_end_to_end.md slice C): a declared VO carries its own
|
|
103
103
|
// lawful sample (the author's validated `example`, the blessed geometry sample, or per-field
|
|
104
104
|
// descriptor synthesis) — the raw-zod walk below cannot see constraints like geo's min-2
|
|
105
105
|
// coordinates and would synthesize a value the law itself refuses.
|
|
106
106
|
const vo = voMetaOf(zt);
|
|
107
|
-
if (vo !== undefined) return voSample(vo);
|
|
107
|
+
if (vo !== undefined) return voSample(vo, { includeOptional: voIncludeOptional });
|
|
108
108
|
switch (zodKind(zt)) {
|
|
109
109
|
case "string":
|
|
110
110
|
return sampleString(zt, field);
|
|
@@ -120,14 +120,14 @@ function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string): unknown {
|
|
|
120
120
|
}
|
|
121
121
|
case "array": {
|
|
122
122
|
const el = zodArrayElement(zt);
|
|
123
|
-
return zodKind(el) === "string" ? ["sample-a"] : [sampleOfZod(el, field, where)];
|
|
123
|
+
return zodKind(el) === "string" ? ["sample-a"] : [sampleOfZod(el, field, where, voIncludeOptional)];
|
|
124
124
|
}
|
|
125
125
|
case "object": {
|
|
126
126
|
const out: Record<string, unknown> = {};
|
|
127
127
|
for (const [name, raw] of Object.entries(zodObjectShape(zt))) {
|
|
128
128
|
const k = zodKind(raw as z.ZodTypeAny);
|
|
129
129
|
if (k === "optional" || k === "default") continue; // optional → omit
|
|
130
|
-
out[name] = sampleOfZod(raw as z.ZodTypeAny, name, where);
|
|
130
|
+
out[name] = sampleOfZod(raw as z.ZodTypeAny, name, where, voIncludeOptional);
|
|
131
131
|
}
|
|
132
132
|
return out;
|
|
133
133
|
}
|
|
@@ -136,10 +136,10 @@ function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string): unknown {
|
|
|
136
136
|
case "nullable":
|
|
137
137
|
case "optional":
|
|
138
138
|
case "default":
|
|
139
|
-
return sampleOfZod(zodDef(zt).innerType!, field, where);
|
|
139
|
+
return sampleOfZod(zodDef(zt).innerType!, field, where, voIncludeOptional);
|
|
140
140
|
case "union": {
|
|
141
141
|
const first = zodDef(zt).options?.[0];
|
|
142
|
-
if (first !== undefined) return sampleOfZod(first, field, where);
|
|
142
|
+
if (first !== undefined) return sampleOfZod(first, field, where, voIncludeOptional);
|
|
143
143
|
throw new Error(`codegen-proof: empty union at field '${field}' of ${where}`);
|
|
144
144
|
}
|
|
145
145
|
default:
|
|
@@ -154,7 +154,7 @@ function sampleOfZod(zt: z.ZodTypeAny, field: string, where: string): unknown {
|
|
|
154
154
|
* optional/defaulted fields omitted, required `…At` strings omitted (auto-stamped
|
|
155
155
|
* by the generated client), everything else sampled by {@link sampleOfZod}.
|
|
156
156
|
*/
|
|
157
|
-
export function synthesizePayload(schema: z.ZodTypeAny, where: string): SynthesizedPayload {
|
|
157
|
+
export function synthesizePayload(schema: z.ZodTypeAny, where: string, opts?: { voIncludeOptional?: boolean }): SynthesizedPayload {
|
|
158
158
|
if (zodKind(schema) !== "object") {
|
|
159
159
|
throw new Error(`codegen-proof: ${where} payload is not a zod object — the proof can only synthesize object payloads`);
|
|
160
160
|
}
|
|
@@ -166,7 +166,7 @@ export function synthesizePayload(schema: z.ZodTypeAny, where: string): Synthesi
|
|
|
166
166
|
const k = zodKind(f);
|
|
167
167
|
if (k === "optional" || k === "default") continue; // optional → omit
|
|
168
168
|
if (stamped.has(name)) continue; // `…At` → omit; the client stamps it
|
|
169
|
-
value[name] = sampleOfZod(f, name, where);
|
|
169
|
+
value[name] = sampleOfZod(f, name, where, opts?.voIncludeOptional === true);
|
|
170
170
|
}
|
|
171
171
|
return { value, autoStamped };
|
|
172
172
|
}
|
|
@@ -598,6 +598,35 @@ export interface BornContextAuthority {
|
|
|
598
598
|
|
|
599
599
|
/** Run a directive's plan with a synthesized+probe payload under birth-probe globals and return the drained
|
|
600
600
|
* field ops (authoring sink + returned ops). `undefined` ⇒ the plan couldn't be probed. */
|
|
601
|
+
/** Run a directive's plan on an EXPLICIT payload under the deterministic probe ports, returning the
|
|
602
|
+
* emitted ops (undefined when the plan throws) — the payload-parameterized sibling of {@link probePlanOps}. */
|
|
603
|
+
function probePlanOpsWithPayload(
|
|
604
|
+
dir: Directive<unknown>,
|
|
605
|
+
probePayload: Record<string, unknown>,
|
|
606
|
+
_where: string,
|
|
607
|
+
): FieldOp[] | undefined {
|
|
608
|
+
let ops: FieldOp[] | undefined;
|
|
609
|
+
try {
|
|
610
|
+
withBirthProbeGlobals(() => {
|
|
611
|
+
__resetAuthoring();
|
|
612
|
+
__resetReads();
|
|
613
|
+
__resetBirths();
|
|
614
|
+
try {
|
|
615
|
+
const returned = dir.plan(probePayload as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
|
|
616
|
+
const drained = [...__drainAuthoring(), ...(Array.isArray(returned) ? returned : [])];
|
|
617
|
+
ops = drained.filter((o): o is FieldOp => (o as { kind?: string }).kind !== "strike");
|
|
618
|
+
} finally {
|
|
619
|
+
__resetAuthoring();
|
|
620
|
+
__resetReads();
|
|
621
|
+
__resetBirths();
|
|
622
|
+
}
|
|
623
|
+
});
|
|
624
|
+
} catch {
|
|
625
|
+
return undefined;
|
|
626
|
+
}
|
|
627
|
+
return ops;
|
|
628
|
+
}
|
|
629
|
+
|
|
601
630
|
function probePlanOps(
|
|
602
631
|
dir: Directive<unknown>,
|
|
603
632
|
where: string,
|
|
@@ -1575,6 +1604,22 @@ export interface ProofLegs {
|
|
|
1575
1604
|
readonly mintField?: string;
|
|
1576
1605
|
readonly query?: { readonly id: string; readonly params: Record<string, unknown> };
|
|
1577
1606
|
readonly count?: { readonly id: string; readonly group: string };
|
|
1607
|
+
/** SPATIAL MEMBERSHIP (lane-2 read-serving): present when a declared spatial index covers the created
|
|
1608
|
+
* aggregate AND the plan-probe proved the synthesized payload lands a geometry on the indexed path —
|
|
1609
|
+
* the offline runner asserts `spatialWithin(id, world)` contains the created row. */
|
|
1610
|
+
readonly spatial?: { readonly id: string };
|
|
1611
|
+
/** EVERY OTHER standalone create (co2 feedback: the proof exercises ALL VO-bearing creates, not just
|
|
1612
|
+
* the first) — each dispatched + asserted exactly like the primary. Additive: absent on older files. */
|
|
1613
|
+
readonly extraCreates?: readonly {
|
|
1614
|
+
readonly directiveId: string;
|
|
1615
|
+
readonly aggregateId: string;
|
|
1616
|
+
readonly payload: Record<string, unknown>;
|
|
1617
|
+
readonly autoStamped: readonly string[];
|
|
1618
|
+
readonly mintField?: string;
|
|
1619
|
+
readonly query?: { readonly id: string; readonly params: Record<string, unknown> };
|
|
1620
|
+
readonly count?: { readonly id: string; readonly group: string };
|
|
1621
|
+
readonly spatial?: { readonly id: string };
|
|
1622
|
+
}[];
|
|
1578
1623
|
/** THE CHILD-BIRTH LEG — present iff this domain has a `.births()` directive (it is `child-birth-provable`).
|
|
1579
1624
|
* When this domain is itself the BORN child's law (no standalone create), the index also registers the
|
|
1580
1625
|
* child domain key pointing at the SAME leg, so `proof --domain <child>` resolves through the parent birth. */
|
|
@@ -1603,14 +1648,69 @@ function isStandaloneCreate(d: Directive<unknown> | undefined): boolean {
|
|
|
1603
1648
|
&& d.requiresRelationGate === undefined;
|
|
1604
1649
|
}
|
|
1605
1650
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
const
|
|
1651
|
+
/** Does `v` carry an RFC 7946 geometry object at `segs` (walked into the composite)? */
|
|
1652
|
+
function geometryAtPath(v: unknown, segs: readonly string[]): boolean {
|
|
1653
|
+
let cur: unknown = v;
|
|
1654
|
+
for (const s of segs) {
|
|
1655
|
+
if (cur === null || typeof cur !== "object") return false;
|
|
1656
|
+
cur = (cur as Record<string, unknown>)[s];
|
|
1657
|
+
}
|
|
1658
|
+
return (
|
|
1659
|
+
cur !== null && typeof cur === "object" &&
|
|
1660
|
+
typeof (cur as { type?: unknown }).type === "string" &&
|
|
1661
|
+
(cur as { coordinates?: unknown }).coordinates !== undefined
|
|
1662
|
+
);
|
|
1663
|
+
}
|
|
1664
|
+
|
|
1665
|
+
/** Build ONE standalone-create leg entry (payload + matched query/count + the spatial-membership
|
|
1666
|
+
* assertion when a declared spatial index covers the type and the plan-probe lands a geometry on
|
|
1667
|
+
* the indexed path — trying the optionals-included VO sample when the default omits it). */
|
|
1668
|
+
function createLegFor(
|
|
1669
|
+
mod: DomainModule,
|
|
1670
|
+
createDir: Directive<unknown>,
|
|
1671
|
+
domain: string,
|
|
1672
|
+
): {
|
|
1673
|
+
directiveId: string; aggregateId: string; payload: Record<string, unknown>;
|
|
1674
|
+
autoStamped: readonly string[]; mintField?: string;
|
|
1675
|
+
query?: { id: string; params: Record<string, unknown> };
|
|
1676
|
+
count?: { id: string; group: string };
|
|
1677
|
+
spatial?: { id: string };
|
|
1678
|
+
} | null {
|
|
1610
1679
|
const agg = (mod.aggregates as AggregateHandle[]).find((a) => a.id === createDir.aggregateId);
|
|
1611
1680
|
if (agg === undefined) return null;
|
|
1612
|
-
const
|
|
1681
|
+
const where = `${domain}/${createDir.id}`;
|
|
1682
|
+
let synth = synthesizePayload(createDir.payloadSchema as z.ZodTypeAny, where);
|
|
1613
1683
|
const mintField = mintedCreateField(createDir, agg);
|
|
1684
|
+
|
|
1685
|
+
// SPATIAL MEMBERSHIP: probe the plan — does the create set the indexed head field with a geometry
|
|
1686
|
+
// at the declared path? If the default sample omits an optional geometry, retry with the
|
|
1687
|
+
// optionals-included VO sample and adopt IT as the leg's payload (so the proof exercises the
|
|
1688
|
+
// geometry the index needs).
|
|
1689
|
+
let spatial: { id: string } | undefined;
|
|
1690
|
+
const spatials = ((mod.spatials ?? []) as { id: string; of: string; on: string }[]).filter((sp) => sp.of === agg.id);
|
|
1691
|
+
if (spatials.length > 0) {
|
|
1692
|
+
const probeWith = (sy: SynthesizedPayload): FieldOp[] | undefined => {
|
|
1693
|
+
const probePayload: Record<string, unknown> = { ...sy.value };
|
|
1694
|
+
for (const f of sy.autoStamped) probePayload[f] = "2026-01-01T00:00:00Z";
|
|
1695
|
+
const probed = probePlanOpsWithPayload(createDir, probePayload, where);
|
|
1696
|
+
return probed;
|
|
1697
|
+
};
|
|
1698
|
+
const hits = (ops: FieldOp[] | undefined, sp: { on: string }): boolean => {
|
|
1699
|
+
if (ops === undefined) return false;
|
|
1700
|
+
const [head, ...segs] = sp.on.split(".");
|
|
1701
|
+
return ops.some((o) => (o as { kind?: string }).kind === "set" && (o as { field?: string }).field === head && geometryAtPath((o as { value?: unknown }).value, segs));
|
|
1702
|
+
};
|
|
1703
|
+
let ops = probeWith(synth);
|
|
1704
|
+
let matched = spatials.find((sp) => hits(ops, sp));
|
|
1705
|
+
if (matched === undefined) {
|
|
1706
|
+
const richer = synthesizePayload(createDir.payloadSchema as z.ZodTypeAny, where, { voIncludeOptional: true });
|
|
1707
|
+
ops = probeWith(richer);
|
|
1708
|
+
matched = spatials.find((sp) => hits(ops, sp));
|
|
1709
|
+
if (matched !== undefined) synth = richer; // the geometry-bearing sample becomes the leg's payload
|
|
1710
|
+
}
|
|
1711
|
+
if (matched !== undefined) spatial = { id: matched.id };
|
|
1712
|
+
}
|
|
1713
|
+
|
|
1614
1714
|
const baseValue: Record<string, unknown> = { ...synth.value };
|
|
1615
1715
|
if (mintField !== undefined) delete baseValue[mintField];
|
|
1616
1716
|
const query = ((mod.queries ?? []) as QueryDecl[]).find(
|
|
@@ -1620,8 +1720,6 @@ export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
|
|
|
1620
1720
|
.map((raw) => finishCount(raw))
|
|
1621
1721
|
.find((c) => c.of === agg.id && (c as { where?: unknown }).where == null && (c.by == null || baseValue[c.by] !== undefined));
|
|
1622
1722
|
return {
|
|
1623
|
-
domain,
|
|
1624
|
-
legs: ["compile-valid", "standalone-provable"],
|
|
1625
1723
|
directiveId: createDir.id,
|
|
1626
1724
|
aggregateId: agg.id,
|
|
1627
1725
|
payload: baseValue,
|
|
@@ -1629,6 +1727,23 @@ export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
|
|
|
1629
1727
|
...(mintField !== undefined ? { mintField } : {}),
|
|
1630
1728
|
...(query ? { query: { id: query.id, params: Object.fromEntries(query.key.map((k) => [k, baseValue[k]])) } } : {}),
|
|
1631
1729
|
...(count ? { count: { id: count.id, group: count.by != null ? String(baseValue[count.by]) : "" } } : {}),
|
|
1730
|
+
...(spatial !== undefined ? { spatial } : {}),
|
|
1731
|
+
};
|
|
1732
|
+
}
|
|
1733
|
+
|
|
1734
|
+
export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
|
|
1735
|
+
const domain = mod.domain ?? mod.name;
|
|
1736
|
+
const creates = (mod.directives as Directive<unknown>[]).filter((d) => isStandaloneCreate(d));
|
|
1737
|
+
const legsBuilt = creates
|
|
1738
|
+
.map((d) => { try { return createLegFor(mod, d, domain); } catch { return null; } })
|
|
1739
|
+
.filter((l): l is NonNullable<typeof l> => l !== null);
|
|
1740
|
+
if (legsBuilt.length === 0) return null;
|
|
1741
|
+
const [primary, ...extras] = legsBuilt;
|
|
1742
|
+
return {
|
|
1743
|
+
domain,
|
|
1744
|
+
legs: ["compile-valid", "standalone-provable"],
|
|
1745
|
+
...primary!,
|
|
1746
|
+
...(extras.length > 0 ? { extraCreates: extras } : {}),
|
|
1632
1747
|
};
|
|
1633
1748
|
}
|
|
1634
1749
|
|
|
@@ -1683,6 +1798,8 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
|
|
|
1683
1798
|
...(standalone.mintField !== undefined ? { mintField: standalone.mintField } : {}),
|
|
1684
1799
|
...(standalone.query !== undefined ? { query: standalone.query } : {}),
|
|
1685
1800
|
...(standalone.count !== undefined ? { count: standalone.count } : {}),
|
|
1801
|
+
...(standalone.spatial !== undefined ? { spatial: standalone.spatial } : {}),
|
|
1802
|
+
...(standalone.extraCreates !== undefined ? { extraCreates: standalone.extraCreates } : {}),
|
|
1686
1803
|
}
|
|
1687
1804
|
: {}),
|
|
1688
1805
|
...(childBirthWithSetup !== undefined ? { childBirth: childBirthWithSetup } : {}),
|
package/src/codegen_ts.ts
CHANGED
|
@@ -438,6 +438,8 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
438
438
|
` watchById(aggregateId: string, cb: (rows: unknown) => void, opts?: { intervalMs?: number }): () => void;`,
|
|
439
439
|
` /** Declared-count / declared-sum O(1) reads (maintained by the read engine). */`,
|
|
440
440
|
` count(countId: string, groupKey?: string): Promise<number>;`,
|
|
441
|
+
` /** THE SPATIAL READ (R*Tree probe): rows intersecting the bbox — same row shape as query(). */`,
|
|
442
|
+
` spatialWithin(spatialId: string, bbox: { minLng: number; minLat: number; maxLng: number; maxLat: number }): Promise<unknown>;`,
|
|
441
443
|
` sum(sumId: string, groupKey?: string): Promise<number>;`,
|
|
442
444
|
...(anyPackedTaxonomy(modules)
|
|
443
445
|
? [
|
|
@@ -824,6 +826,12 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
824
826
|
}
|
|
825
827
|
}
|
|
826
828
|
|
|
829
|
+
for (const sp of (mod.spatials ?? []) as { id: string; of: string; on: string }[]) {
|
|
830
|
+
methods.push(
|
|
831
|
+
` /** Declared spatial index \`${sp.id}\` — rows of \`${sp.of}\` whose \`${sp.on}\` geometry intersects the bbox (R*Tree probe). */`,
|
|
832
|
+
` ${lcFirst(camel(sp.id))}: (bbox: { minLng: number; minLat: number; maxLng: number; maxLat: number }) => holon.spatialWithin(${JSON.stringify(sp.id)}, bbox) as Promise<Row<${sp.of}Data>[]>,`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
827
835
|
for (const q of (mod.queries ?? []) as QueryDecl[]) {
|
|
828
836
|
const agg = aggByType.get(q.returns);
|
|
829
837
|
const params = q.key
|
package/src/value_object.ts
CHANGED
|
@@ -349,6 +349,7 @@ function sampleOfDescriptorField(
|
|
|
349
349
|
f: ValueObjectFieldDescriptor,
|
|
350
350
|
fieldName: string,
|
|
351
351
|
byName: Map<string, ValueObjectDescriptor>,
|
|
352
|
+
includeOptional = false,
|
|
352
353
|
): unknown {
|
|
353
354
|
switch (f.type) {
|
|
354
355
|
case "string": return `sample-${fieldName}`;
|
|
@@ -356,17 +357,17 @@ function sampleOfDescriptorField(
|
|
|
356
357
|
case "literal": return f.value;
|
|
357
358
|
case "boolean": return true;
|
|
358
359
|
case "enum": return f.values[0];
|
|
359
|
-
case "array": return [sampleOfDescriptorField(f.items, fieldName, byName)];
|
|
360
|
+
case "array": return [sampleOfDescriptorField(f.items, fieldName, byName, includeOptional)];
|
|
360
361
|
case "geometry": return GEOMETRY_SAMPLES[f.geometry];
|
|
361
362
|
case "object": {
|
|
362
363
|
const dep = byName.get(f.ref);
|
|
363
364
|
if (dep === undefined) throw new Error(`voSample: nested value object '${f.ref}' has no descriptor`);
|
|
364
|
-
return sampleOfDescriptor(dep, byName);
|
|
365
|
+
return sampleOfDescriptor(dep, byName, includeOptional);
|
|
365
366
|
}
|
|
366
367
|
}
|
|
367
368
|
}
|
|
368
369
|
|
|
369
|
-
function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueObjectDescriptor
|
|
370
|
+
function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueObjectDescriptor>, includeOptional = false): unknown {
|
|
370
371
|
if (d.geometry !== undefined) return GEOMETRY_SAMPLES[d.geometry];
|
|
371
372
|
if (d.union !== undefined) {
|
|
372
373
|
// FIRST declared arm (keys are sorted — deterministic); `example:` overrides upstream.
|
|
@@ -378,15 +379,15 @@ function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueO
|
|
|
378
379
|
}
|
|
379
380
|
const out: Record<string, unknown> = { type: key };
|
|
380
381
|
for (const [k, f] of Object.entries(arm.fields)) {
|
|
381
|
-
if (f.optional === true) continue;
|
|
382
|
-
out[k] = sampleOfDescriptorField(f, k, byName);
|
|
382
|
+
if (f.optional === true && !includeOptional) continue;
|
|
383
|
+
out[k] = sampleOfDescriptorField(f, k, byName, includeOptional);
|
|
383
384
|
}
|
|
384
385
|
return out;
|
|
385
386
|
}
|
|
386
387
|
const out: Record<string, unknown> = {};
|
|
387
388
|
for (const [k, f] of Object.entries(d.fields ?? {})) {
|
|
388
|
-
if (f.optional === true) continue; // optional → omitted (mirrors payload synthesis)
|
|
389
|
-
out[k] = sampleOfDescriptorField(f, k, byName);
|
|
389
|
+
if (f.optional === true && !includeOptional) continue; // optional → omitted (mirrors payload synthesis)
|
|
390
|
+
out[k] = sampleOfDescriptorField(f, k, byName, includeOptional);
|
|
390
391
|
}
|
|
391
392
|
return out;
|
|
392
393
|
}
|
|
@@ -397,12 +398,12 @@ function sampleOfDescriptor(d: ValueObjectDescriptor, byName: Map<string, ValueO
|
|
|
397
398
|
* VOs). Proof synthesis calls this instead of guessing at the raw zod — which is exactly the
|
|
398
399
|
* lane that refused its own sample on constrained shapes (geo min-2 coordinates).
|
|
399
400
|
*/
|
|
400
|
-
export function voSample(meta: ValueObjectFieldMeta): unknown {
|
|
401
|
+
export function voSample(meta: ValueObjectFieldMeta, opts?: { includeOptional?: boolean }): unknown {
|
|
401
402
|
if (meta.example !== undefined) return meta.example;
|
|
402
403
|
const byName = new Map<string, ValueObjectDescriptor>();
|
|
403
404
|
for (const d of meta.dependencies) byName.set(d.name, d);
|
|
404
405
|
byName.set(meta.descriptor.name, meta.descriptor);
|
|
405
|
-
return sampleOfDescriptor(meta.descriptor, byName);
|
|
406
|
+
return sampleOfDescriptor(meta.descriptor, byName, opts?.includeOptional === true);
|
|
406
407
|
}
|
|
407
408
|
|
|
408
409
|
function makeValueObject<T>(
|