@githolon/dsl 0.43.0 → 0.45.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.43.0",
3
+ "version": "0.45.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",
@@ -238,15 +238,63 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
238
238
  // hand-rolled `t.jsonObject()` GeoJSON leaf) pass through unchecked, exactly as before.
239
239
  for (const s of spatials) {
240
240
  const agg = byId.get(s.of);
241
- const field = agg?.fields[s.on];
242
- const vo = field?.valueObject;
243
- if (vo !== undefined && vo.geometry === undefined) {
241
+ const [head, ...pathSegs] = s.on.split(".");
242
+ const field = agg?.fields[head!];
243
+ if (agg !== undefined && field === undefined) {
244
244
  throw new Error(
245
- `build-package: spatial '${s.id}' indexes '${s.of}.${s.on}', a '${vo.name}' value object with NO ` +
246
- `geometry — the R*Tree needs an RFC 7946 geometry leaf. Give the geometry its OWN field ` +
247
- `(e.g. \`point: geo.point().field()\`) and index that (v1: no sub-path indexing).`,
245
+ `build-package: spatial '${s.id}' indexes '${s.of}.${s.on}', but '${head}' is not a declared field of '${s.of}'.`,
248
246
  );
249
247
  }
248
+ const vo = field?.valueObject;
249
+ if (pathSegs.length === 0) {
250
+ if (vo !== undefined && vo.geometry === undefined) {
251
+ throw new Error(
252
+ `build-package: spatial '${s.id}' indexes '${s.of}.${s.on}', a '${vo.name}' value object with NO ` +
253
+ `geometry — the R*Tree needs an RFC 7946 geometry leaf. Index the nested geometry by path ` +
254
+ `(e.g. \`.on("${s.on}.point")\`) or give the geometry its OWN field.`,
255
+ );
256
+ }
257
+ continue;
258
+ }
259
+ // SUB-PATH SPATIAL (lane 2): the path is walked inside the composite leaf by the projection.
260
+ // When the head is a DECLARED VO, prove at build that the path lands on an RFC 7946 geometry
261
+ // (typo'd paths refuse here, not silently index nothing). A plain t.jsonObject() head passes
262
+ // unchecked — the same trust posture as a top-level hand-rolled geometry leaf.
263
+ if (vo !== undefined) {
264
+ const byName = new Map(vo.dependencies.map((d) => [d.name, d]));
265
+ byName.set(vo.descriptor.name, vo.descriptor);
266
+ let cur = vo.descriptor;
267
+ for (let i = 0; i < pathSegs.length; i++) {
268
+ const seg = pathSegs[i]!;
269
+ const f = cur.fields?.[seg];
270
+ if (f === undefined) {
271
+ throw new Error(
272
+ `build-package: spatial '${s.id}' path '${s.on}' — '${seg}' is not a field of value object '${cur.name}'.`,
273
+ );
274
+ }
275
+ const last = i === pathSegs.length - 1;
276
+ if (last) {
277
+ if (f.type !== "geometry") {
278
+ throw new Error(
279
+ `build-package: spatial '${s.id}' path '${s.on}' lands on a '${f.type}' field — the R*Tree needs ` +
280
+ `an RFC 7946 geometry leaf (a \`geo.*\` value object) at the end of the path.`,
281
+ );
282
+ }
283
+ } else {
284
+ if (f.type !== "object") {
285
+ throw new Error(
286
+ `build-package: spatial '${s.id}' path '${s.on}' — '${seg}' is a '${f.type}' field, not a nested ` +
287
+ `value object; the path cannot continue through it.`,
288
+ );
289
+ }
290
+ const next = byName.get(f.ref);
291
+ if (next === undefined) {
292
+ throw new Error(`build-package: spatial '${s.id}' path '${s.on}' — no descriptor for nested VO '${f.ref}'.`);
293
+ }
294
+ cur = next;
295
+ }
296
+ }
297
+ }
250
298
  }
251
299
  const dartImports = [...(spec.dartImports ?? [])];
252
300
  // THE PEER-SOVEREIGN ROOT-OF-TRUST: a home/child law that exports `requireParentAttestedBirth` declares
@@ -1475,6 +1475,8 @@ function emitUnionClasses(d: ValueObjectDescriptor): string {
1475
1475
  `/// Union \`${d.name}\` — a sum type stored as ONE json leaf (wholesale-replace; the`,
1476
1476
  `/// \`type\` discriminator names the arm). Adding an arm is ADDITIVE (older clients decode`,
1477
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).`,
1478
1480
  `sealed class ${d.name} {`,
1479
1481
  ` const ${d.name}();`,
1480
1482
  ` Map<String, Object?> toJson();`,
package/src/flow_gate.ts CHANGED
@@ -173,8 +173,19 @@ export function assertFlowGate(m: CanonicalManifest, mod?: FlowModule): void {
173
173
  let refs: readonly { readonly aggregate: string }[];
174
174
  try {
175
175
  refs = wi.reads({ intent: new Proxy({}, { get: () => "" }) as Record<string, unknown>, principal: "" }) ?? [];
176
- } catch {
177
- continue; // the read-set can't be resolved on a probe intent — skip (no false refusal; conservative on availability)
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
+ );
178
189
  }
179
190
  for (const r of refs) {
180
191
  const short = r.aggregate.includes(".") ? r.aggregate.slice(r.aggregate.lastIndexOf(".") + 1) : r.aggregate;
package/src/spatial.ts CHANGED
@@ -72,8 +72,14 @@ export interface OfSpatial<F extends Record<string, Field> = Record<string, Fiel
72
72
  * declaration. `field` is `keyof`-checked against the `of`-aggregate's fields — a
73
73
  * typo'd field is a compile error (the same convention as `count`'s `.by(...)` is
74
74
  * a field of the type / `query`'s key fields).
75
+ *
76
+ * SUB-PATH SPATIAL (lane 2): `field` may be a DOTTED PATH into a composite value-object
77
+ * leaf (`"location.point"`) — the projection walks the decoded JSON to the nested
78
+ * geometry natively (one copy of the truth; NO denormalized sibling field). The head
79
+ * must be a declared field; when it is a declared VO, the sub-path is descriptor-checked
80
+ * at build to land on an RFC 7946 geometry.
75
81
  */
76
- on(field: StringKeyOf<F>): SpatialDecl;
82
+ on(field: StringKeyOf<F> | `${StringKeyOf<F>}.${string}`): SpatialDecl;
77
83
  }
78
84
 
79
85
  /**
@@ -630,9 +630,10 @@ function geoZod(kind: GeometryKind, coordinates: z.ZodTypeAny): z.ZodTypeAny {
630
630
  * The GEO value objects — RFC 7946 geometries as named VOs, and THE BLESSED WAY to
631
631
  * declare a spatial-indexable field: give the geometry its OWN aggregate field
632
632
  * (`point: GeoPoint.field()`) and pair it with `spatial(id).of(Agg).on("point")` —
633
- * the R*Tree indexes the field's GeoJSON geometry directly. (v1 boundary: the spatial
634
- * lane indexes a TOP-LEVEL field; a geometry nested inside another VO — e.g.
635
- * `location.point` — is data, not an indexable path.)
633
+ * the R*Tree indexes the field's GeoJSON geometry directly. SUB-PATH SPATIAL (lane 2):
634
+ * a geometry NESTED inside a composite VO is ALSO indexable by dotted path —
635
+ * `spatial(id).of(Agg).on("location.point")` — the projection walks the decoded leaf to the
636
+ * geometry natively (one copy of the truth; requires kernel ≥ the lane-2 artifact).
636
637
  */
637
638
  export const geo = {
638
639
  /** An RFC 7946 **Point** (`{type:"Point", coordinates:[lng,lat(,alt)?]}`) — the v1