@githolon/dsl 0.45.0 → 0.47.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/errors.dart +250 -0
- package/dart/lib/src/test_harness.dart +182 -175
- package/dart/lib/src/webview_bridge.dart +38 -4
- package/dart/pubspec.yaml +1 -1
- package/dart/test/errors_test.dart +77 -0
- package/dart/test/test_harness_test.dart +85 -57
- package/package.json +1 -1
- package/src/codegen_dart.ts +25 -14
- package/src/codegen_dart_client.ts +33 -1
- package/src/codegen_proof.ts +255 -23
- package/src/codegen_ts.ts +33 -0
- package/src/compat_manifest.ts +20 -0
- package/src/compile_package_main.ts +174 -16
- package/src/framework/framework_aggregates.ts +18 -0
- package/src/read_projection_composition.ts +9 -9
- package/src/read_projection_namespace.ts +4 -5
- package/src/usd.ts +3 -3
- package/src/value_object.ts +10 -9
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,
|
|
@@ -1257,6 +1286,8 @@ export function diagnoseRelationBootstrapGap(mod: DomainModule): string | undefi
|
|
|
1257
1286
|
interface DiscoveredGenesisStep {
|
|
1258
1287
|
readonly domain: string;
|
|
1259
1288
|
readonly directiveId: string;
|
|
1289
|
+
/** The step's dispatch payload — validated at compile against the target directive's schema (births DX). */
|
|
1290
|
+
readonly payload?: unknown;
|
|
1260
1291
|
}
|
|
1261
1292
|
|
|
1262
1293
|
/** A discovered CHILD-BIRTH leg: a parent `.births()` directive proven end-to-end into the child it births
|
|
@@ -1304,6 +1335,12 @@ export interface ChildBirthLeg {
|
|
|
1304
1335
|
* parent workspace (home → estate is the canonical case), the proof runner drives this setup birth first
|
|
1305
1336
|
* and then authors the child birth inside the born parent. */
|
|
1306
1337
|
readonly parentSetup?: ChildBirthParentSetup;
|
|
1338
|
+
/** COMPILE-TIME RECIPE DIAGNOSTICS (births DX): problems found by statically validating each genesis
|
|
1339
|
+
* step against the composed package (missing child module, unknown seed directive, payload-schema
|
|
1340
|
+
* mismatch, a plan that returns a non-array — the exact things that otherwise surface at proof time as
|
|
1341
|
+
* an opaque `born=[]`). WARNINGS, not refusals: the kernel's own gate stays the arbiter; the compile
|
|
1342
|
+
* summary prints these and the proof runner fails fast on them with the same words. */
|
|
1343
|
+
readonly recipeIssues?: readonly string[];
|
|
1307
1344
|
}
|
|
1308
1345
|
|
|
1309
1346
|
export interface ChildBirthParentSetup {
|
|
@@ -1362,13 +1399,94 @@ export function discoverChildBirthLeg(
|
|
|
1362
1399
|
parentDomain: string,
|
|
1363
1400
|
modules: readonly DomainModule[],
|
|
1364
1401
|
): ChildBirthLeg | undefined {
|
|
1365
|
-
|
|
1402
|
+
const r = discoverChildBirthLegOrReason(birthDir, parentDomain, modules);
|
|
1403
|
+
return r.leg;
|
|
1404
|
+
}
|
|
1405
|
+
|
|
1406
|
+
/** The engine iterates a plan's return value unconditionally (`wire_encode.ts`: `[...returned]`), so a
|
|
1407
|
+
* plan returning a builder / undefined ALWAYS halts at the gate with an opaque "value is not iterable".
|
|
1408
|
+
* This hint turns that into the fix. */
|
|
1409
|
+
const PLAN_RETURN_HINT =
|
|
1410
|
+
"a plan must RETURN AN ARRAY of ops — fluent builders (ensure()/create().set(...)) record themselves, so end the plan with `return [];`, or wrap standalone ops in `[ ... ]`";
|
|
1411
|
+
|
|
1412
|
+
/** STATIC RECIPE VALIDATION (births DX): check each genesis step against the composed package — the same
|
|
1413
|
+
* checks the child's own gate will make at birth, surfaced at COMPILE with a fix in the message. The
|
|
1414
|
+
* bootstrap/nomos `installDomain` steps are kernel-resolved (bytes-by-hash) and skipped. */
|
|
1415
|
+
function validateGenesisSteps(
|
|
1416
|
+
chain: readonly DiscoveredGenesisStep[],
|
|
1417
|
+
modules: readonly DomainModule[],
|
|
1418
|
+
): string[] {
|
|
1419
|
+
const issues: string[] = [];
|
|
1420
|
+
chain.forEach((s, i) => {
|
|
1421
|
+
const at = `genesis step ${i + 1}`;
|
|
1422
|
+
if (s === null || typeof s !== "object" || typeof s.domain !== "string" || typeof s.directiveId !== "string") {
|
|
1423
|
+
issues.push(`${at} is not a {domain, directiveId, actor, payload} step object`);
|
|
1424
|
+
return;
|
|
1425
|
+
}
|
|
1426
|
+
if (s.directiveId === "installDomain" && (s.domain === "bootstrap" || s.domain === "nomos")) return;
|
|
1427
|
+
const mod = modules.find((m) => (m.domain ?? m.name) === s.domain);
|
|
1428
|
+
if (mod === undefined) {
|
|
1429
|
+
issues.push(
|
|
1430
|
+
`${at} (${s.domain}/${s.directiveId}) targets domain '${s.domain}', which is NOT composed in this package — add its module to nomos.package.mjs \`domains\` (the one-package pattern: the parent must fold the child law so the kernel can resolve its bytes by hash; see bench/scale/fixtures/estate-birth)`,
|
|
1431
|
+
);
|
|
1432
|
+
return;
|
|
1433
|
+
}
|
|
1434
|
+
const dir = (mod.directives as Directive<unknown>[]).find((d) => d?.id === s.directiveId);
|
|
1435
|
+
if (dir === undefined) {
|
|
1436
|
+
issues.push(
|
|
1437
|
+
`${at} (${s.domain}/${s.directiveId}): domain '${s.domain}' has no directive '${s.directiveId}' — it has: ${mod.directives.map((d) => d.id).join(", ")}`,
|
|
1438
|
+
);
|
|
1439
|
+
return;
|
|
1440
|
+
}
|
|
1441
|
+
const parsed = (dir.payloadSchema as z.ZodTypeAny).safeParse(s.payload);
|
|
1442
|
+
if (!parsed.success) {
|
|
1443
|
+
const why = parsed.error.issues.map((iss) => `${iss.path.join(".") || "(payload)"}: ${iss.message}`).join("; ");
|
|
1444
|
+
issues.push(`${at} (${s.domain}/${s.directiveId}): the recipe payload does not satisfy the directive's payload schema — ${why}`);
|
|
1445
|
+
return;
|
|
1446
|
+
}
|
|
1447
|
+
// Run the step's plan on the recipe's own payload (the exact dispatch the child gate replays) and
|
|
1448
|
+
// enforce the ENGINE's return contract: the sealed engine spreads the return value, so a non-array
|
|
1449
|
+
// return is ALWAYS a runtime halt — catch it here, with the fix.
|
|
1450
|
+
try {
|
|
1451
|
+
withBirthProbeGlobals(() => {
|
|
1452
|
+
__resetAuthoring();
|
|
1453
|
+
__resetReads();
|
|
1454
|
+
__resetBirths();
|
|
1455
|
+
try {
|
|
1456
|
+
const returned = dir.plan(s.payload as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
|
|
1457
|
+
if (!Array.isArray(returned)) {
|
|
1458
|
+
issues.push(
|
|
1459
|
+
`${at} (${s.domain}/${s.directiveId}): the plan returned ${returned === undefined ? "undefined" : `a non-array (${typeof returned})`} — ${PLAN_RETURN_HINT}`,
|
|
1460
|
+
);
|
|
1461
|
+
}
|
|
1462
|
+
} finally {
|
|
1463
|
+
try { __drainAuthoring(); __drainReads(); __drainBirths(); } catch { /* drained */ }
|
|
1464
|
+
}
|
|
1465
|
+
});
|
|
1466
|
+
} catch (e) {
|
|
1467
|
+
issues.push(
|
|
1468
|
+
`${at} (${s.domain}/${s.directiveId}): the plan THREW at the compile probe (${(e as Error)?.message ?? e}) — if it only fails on the sample payload this is a false alarm, but the child gate will replay this exact step at birth`,
|
|
1469
|
+
);
|
|
1470
|
+
}
|
|
1471
|
+
});
|
|
1472
|
+
return issues;
|
|
1473
|
+
}
|
|
1474
|
+
|
|
1475
|
+
/** {@link discoverChildBirthLeg} with a REASON on the undiscoverable path — so `.births()` without a
|
|
1476
|
+
* provable leg prints WHY at compile instead of a silent \`proof SKIPPED\`. */
|
|
1477
|
+
export function discoverChildBirthLegOrReason(
|
|
1478
|
+
birthDir: Directive<unknown>,
|
|
1479
|
+
parentDomain: string,
|
|
1480
|
+
modules: readonly DomainModule[],
|
|
1481
|
+
): { leg?: ChildBirthLeg; reason?: string } {
|
|
1482
|
+
if (birthDir.birthsChild !== true) return {};
|
|
1483
|
+
const who = `${parentDomain}/${birthDir.id}`;
|
|
1366
1484
|
let recipe: { workspace?: unknown; genesisChain?: unknown } | undefined;
|
|
1367
1485
|
let synth: SynthesizedPayload;
|
|
1368
1486
|
try {
|
|
1369
1487
|
synth = synthesizePayload(birthDir.payloadSchema as z.ZodTypeAny, `${parentDomain}/${birthDir.id}`);
|
|
1370
|
-
} catch {
|
|
1371
|
-
return
|
|
1488
|
+
} catch (e) {
|
|
1489
|
+
return { reason: `${who}: the payload could not be synthesized for the compile probe (${(e as Error)?.message ?? e}) — the offline proof needs a sampleable payload (strings/numbers/enums; exotic shapes drop the leg)` };
|
|
1372
1490
|
}
|
|
1373
1491
|
// The plan needs concrete payload VALUES for every field it reads to build the recipe (frameworkHash /
|
|
1374
1492
|
// lawHash / owner …). The synthesizer omits `…At` (auto-stamped) + optional fields; fill the auto-stamped
|
|
@@ -1389,10 +1507,15 @@ export function discoverChildBirthLeg(
|
|
|
1389
1507
|
const drained = __drainBirths();
|
|
1390
1508
|
recipe = drained[0] as typeof recipe;
|
|
1391
1509
|
});
|
|
1392
|
-
} catch {
|
|
1393
|
-
return
|
|
1510
|
+
} catch (e) {
|
|
1511
|
+
return { reason: `${who}: the plan THREW at the compile probe (${(e as Error)?.message ?? e}) — the probe runs it on a synthesized payload to read the birth recipe` };
|
|
1512
|
+
}
|
|
1513
|
+
if (recipe === undefined) {
|
|
1514
|
+
return { reason: `${who}: the plan declared .births() but recorded NO birth — its plan must call birth({ workspace, genesisChain }) (see dsl/src/framework/home.ts birthHome, or bench/scale/fixtures/estate-birth)` };
|
|
1515
|
+
}
|
|
1516
|
+
if (!Array.isArray(recipe.genesisChain)) {
|
|
1517
|
+
return { reason: `${who}: birth() was called without a genesisChain ARRAY — the recipe is { workspace, genesisChain: [ ...steps ] }` };
|
|
1394
1518
|
}
|
|
1395
|
-
if (recipe === undefined || !Array.isArray(recipe.genesisChain)) return undefined;
|
|
1396
1519
|
const chain = recipe.genesisChain as DiscoveredGenesisStep[];
|
|
1397
1520
|
// The child seed = the FIRST genesis step that is neither bootstrap nor nomos installDomain (the leaf's
|
|
1398
1521
|
// own seed). A platform-shaped recipe (seedGenesis → grantAdmin → birthCloud tail) is out of scope here —
|
|
@@ -1401,7 +1524,10 @@ export function discoverChildBirthLeg(
|
|
|
1401
1524
|
(s) => s !== null && typeof s === "object" && s.directiveId !== "installDomain"
|
|
1402
1525
|
&& s.domain !== "bootstrap" && s.domain !== "nomos",
|
|
1403
1526
|
);
|
|
1404
|
-
if (seedStep === undefined)
|
|
1527
|
+
if (seedStep === undefined) {
|
|
1528
|
+
return { reason: `${who}: the genesis chain has ONLY install steps (no child seed) — the offline leg proves the LEAF shape [bootstrap/installDomain, nomos/installDomain, <childDomain>/<seedDirective>]; add the child's own seed step (a platform-shaped seedGenesis→grantAdmin→birthCloud tail is proven live, not offline)` };
|
|
1529
|
+
}
|
|
1530
|
+
const recipeIssues = validateGenesisSteps(chain, modules);
|
|
1405
1531
|
const childDomain = seedStep.domain;
|
|
1406
1532
|
const childSeedDirectiveId = seedStep.directiveId;
|
|
1407
1533
|
|
|
@@ -1452,7 +1578,7 @@ export function discoverChildBirthLeg(
|
|
|
1452
1578
|
if (frameworkHashField !== undefined) delete birthPayload[frameworkHashField];
|
|
1453
1579
|
if (lawHashField !== undefined) delete birthPayload[lawHashField];
|
|
1454
1580
|
|
|
1455
|
-
|
|
1581
|
+
const leg: ChildBirthLeg = {
|
|
1456
1582
|
parentDomain,
|
|
1457
1583
|
birthDirectiveId: birthDir.id,
|
|
1458
1584
|
birthPayload,
|
|
@@ -1466,7 +1592,9 @@ export function discoverChildBirthLeg(
|
|
|
1466
1592
|
...(childSeedAggregateId !== undefined ? { childSeedAggregateId } : {}),
|
|
1467
1593
|
...(childSeedInstanceId !== undefined ? { childSeedInstanceId } : {}),
|
|
1468
1594
|
...(childQuery !== undefined ? { childQuery } : {}),
|
|
1595
|
+
...(recipeIssues.length > 0 ? { recipeIssues } : {}),
|
|
1469
1596
|
};
|
|
1597
|
+
return { leg };
|
|
1470
1598
|
}
|
|
1471
1599
|
|
|
1472
1600
|
function childBirthsForModule(mod: DomainModule, modules: readonly DomainModule[]): ChildBirthLeg[] {
|
|
@@ -1575,6 +1703,27 @@ export interface ProofLegs {
|
|
|
1575
1703
|
readonly mintField?: string;
|
|
1576
1704
|
readonly query?: { readonly id: string; readonly params: Record<string, unknown> };
|
|
1577
1705
|
readonly count?: { readonly id: string; readonly group: string };
|
|
1706
|
+
/** BIRTHS DX DIAGNOSTICS: present when this domain declares ≥1 `.births()` directive whose child-birth
|
|
1707
|
+
* leg is either UNDISCOVERABLE (the reason, so the compile summary never silently skips a birth) or
|
|
1708
|
+
* discoverable-but-suspect (the leg's `recipeIssues`). Printed by the compile summary; the offline
|
|
1709
|
+
* runner fails fast on recipe issues with the same words. Additive: absent on older files. */
|
|
1710
|
+
readonly birthDiagnostics?: readonly string[];
|
|
1711
|
+
/** SPATIAL MEMBERSHIP (lane-2 read-serving): present when a declared spatial index covers the created
|
|
1712
|
+
* aggregate AND the plan-probe proved the synthesized payload lands a geometry on the indexed path —
|
|
1713
|
+
* the offline runner asserts `spatialWithin(id, world)` contains the created row. */
|
|
1714
|
+
readonly spatial?: { readonly id: string };
|
|
1715
|
+
/** EVERY OTHER standalone create (co2 feedback: the proof exercises ALL VO-bearing creates, not just
|
|
1716
|
+
* the first) — each dispatched + asserted exactly like the primary. Additive: absent on older files. */
|
|
1717
|
+
readonly extraCreates?: readonly {
|
|
1718
|
+
readonly directiveId: string;
|
|
1719
|
+
readonly aggregateId: string;
|
|
1720
|
+
readonly payload: Record<string, unknown>;
|
|
1721
|
+
readonly autoStamped: readonly string[];
|
|
1722
|
+
readonly mintField?: string;
|
|
1723
|
+
readonly query?: { readonly id: string; readonly params: Record<string, unknown> };
|
|
1724
|
+
readonly count?: { readonly id: string; readonly group: string };
|
|
1725
|
+
readonly spatial?: { readonly id: string };
|
|
1726
|
+
}[];
|
|
1578
1727
|
/** THE CHILD-BIRTH LEG — present iff this domain has a `.births()` directive (it is `child-birth-provable`).
|
|
1579
1728
|
* When this domain is itself the BORN child's law (no standalone create), the index also registers the
|
|
1580
1729
|
* child domain key pointing at the SAME leg, so `proof --domain <child>` resolves through the parent birth. */
|
|
@@ -1603,14 +1752,69 @@ function isStandaloneCreate(d: Directive<unknown> | undefined): boolean {
|
|
|
1603
1752
|
&& d.requiresRelationGate === undefined;
|
|
1604
1753
|
}
|
|
1605
1754
|
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
const
|
|
1755
|
+
/** Does `v` carry an RFC 7946 geometry object at `segs` (walked into the composite)? */
|
|
1756
|
+
function geometryAtPath(v: unknown, segs: readonly string[]): boolean {
|
|
1757
|
+
let cur: unknown = v;
|
|
1758
|
+
for (const s of segs) {
|
|
1759
|
+
if (cur === null || typeof cur !== "object") return false;
|
|
1760
|
+
cur = (cur as Record<string, unknown>)[s];
|
|
1761
|
+
}
|
|
1762
|
+
return (
|
|
1763
|
+
cur !== null && typeof cur === "object" &&
|
|
1764
|
+
typeof (cur as { type?: unknown }).type === "string" &&
|
|
1765
|
+
(cur as { coordinates?: unknown }).coordinates !== undefined
|
|
1766
|
+
);
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
/** Build ONE standalone-create leg entry (payload + matched query/count + the spatial-membership
|
|
1770
|
+
* assertion when a declared spatial index covers the type and the plan-probe lands a geometry on
|
|
1771
|
+
* the indexed path — trying the optionals-included VO sample when the default omits it). */
|
|
1772
|
+
function createLegFor(
|
|
1773
|
+
mod: DomainModule,
|
|
1774
|
+
createDir: Directive<unknown>,
|
|
1775
|
+
domain: string,
|
|
1776
|
+
): {
|
|
1777
|
+
directiveId: string; aggregateId: string; payload: Record<string, unknown>;
|
|
1778
|
+
autoStamped: readonly string[]; mintField?: string;
|
|
1779
|
+
query?: { id: string; params: Record<string, unknown> };
|
|
1780
|
+
count?: { id: string; group: string };
|
|
1781
|
+
spatial?: { id: string };
|
|
1782
|
+
} | null {
|
|
1610
1783
|
const agg = (mod.aggregates as AggregateHandle[]).find((a) => a.id === createDir.aggregateId);
|
|
1611
1784
|
if (agg === undefined) return null;
|
|
1612
|
-
const
|
|
1785
|
+
const where = `${domain}/${createDir.id}`;
|
|
1786
|
+
let synth = synthesizePayload(createDir.payloadSchema as z.ZodTypeAny, where);
|
|
1613
1787
|
const mintField = mintedCreateField(createDir, agg);
|
|
1788
|
+
|
|
1789
|
+
// SPATIAL MEMBERSHIP: probe the plan — does the create set the indexed head field with a geometry
|
|
1790
|
+
// at the declared path? If the default sample omits an optional geometry, retry with the
|
|
1791
|
+
// optionals-included VO sample and adopt IT as the leg's payload (so the proof exercises the
|
|
1792
|
+
// geometry the index needs).
|
|
1793
|
+
let spatial: { id: string } | undefined;
|
|
1794
|
+
const spatials = ((mod.spatials ?? []) as { id: string; of: string; on: string }[]).filter((sp) => sp.of === agg.id);
|
|
1795
|
+
if (spatials.length > 0) {
|
|
1796
|
+
const probeWith = (sy: SynthesizedPayload): FieldOp[] | undefined => {
|
|
1797
|
+
const probePayload: Record<string, unknown> = { ...sy.value };
|
|
1798
|
+
for (const f of sy.autoStamped) probePayload[f] = "2026-01-01T00:00:00Z";
|
|
1799
|
+
const probed = probePlanOpsWithPayload(createDir, probePayload, where);
|
|
1800
|
+
return probed;
|
|
1801
|
+
};
|
|
1802
|
+
const hits = (ops: FieldOp[] | undefined, sp: { on: string }): boolean => {
|
|
1803
|
+
if (ops === undefined) return false;
|
|
1804
|
+
const [head, ...segs] = sp.on.split(".");
|
|
1805
|
+
return ops.some((o) => (o as { kind?: string }).kind === "set" && (o as { field?: string }).field === head && geometryAtPath((o as { value?: unknown }).value, segs));
|
|
1806
|
+
};
|
|
1807
|
+
let ops = probeWith(synth);
|
|
1808
|
+
let matched = spatials.find((sp) => hits(ops, sp));
|
|
1809
|
+
if (matched === undefined) {
|
|
1810
|
+
const richer = synthesizePayload(createDir.payloadSchema as z.ZodTypeAny, where, { voIncludeOptional: true });
|
|
1811
|
+
ops = probeWith(richer);
|
|
1812
|
+
matched = spatials.find((sp) => hits(ops, sp));
|
|
1813
|
+
if (matched !== undefined) synth = richer; // the geometry-bearing sample becomes the leg's payload
|
|
1814
|
+
}
|
|
1815
|
+
if (matched !== undefined) spatial = { id: matched.id };
|
|
1816
|
+
}
|
|
1817
|
+
|
|
1614
1818
|
const baseValue: Record<string, unknown> = { ...synth.value };
|
|
1615
1819
|
if (mintField !== undefined) delete baseValue[mintField];
|
|
1616
1820
|
const query = ((mod.queries ?? []) as QueryDecl[]).find(
|
|
@@ -1620,8 +1824,6 @@ export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
|
|
|
1620
1824
|
.map((raw) => finishCount(raw))
|
|
1621
1825
|
.find((c) => c.of === agg.id && (c as { where?: unknown }).where == null && (c.by == null || baseValue[c.by] !== undefined));
|
|
1622
1826
|
return {
|
|
1623
|
-
domain,
|
|
1624
|
-
legs: ["compile-valid", "standalone-provable"],
|
|
1625
1827
|
directiveId: createDir.id,
|
|
1626
1828
|
aggregateId: agg.id,
|
|
1627
1829
|
payload: baseValue,
|
|
@@ -1629,6 +1831,23 @@ export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
|
|
|
1629
1831
|
...(mintField !== undefined ? { mintField } : {}),
|
|
1630
1832
|
...(query ? { query: { id: query.id, params: Object.fromEntries(query.key.map((k) => [k, baseValue[k]])) } } : {}),
|
|
1631
1833
|
...(count ? { count: { id: count.id, group: count.by != null ? String(baseValue[count.by]) : "" } } : {}),
|
|
1834
|
+
...(spatial !== undefined ? { spatial } : {}),
|
|
1835
|
+
};
|
|
1836
|
+
}
|
|
1837
|
+
|
|
1838
|
+
export function proofLegsForModule(mod: DomainModule): ProofLegs | null {
|
|
1839
|
+
const domain = mod.domain ?? mod.name;
|
|
1840
|
+
const creates = (mod.directives as Directive<unknown>[]).filter((d) => isStandaloneCreate(d));
|
|
1841
|
+
const legsBuilt = creates
|
|
1842
|
+
.map((d) => { try { return createLegFor(mod, d, domain); } catch { return null; } })
|
|
1843
|
+
.filter((l): l is NonNullable<typeof l> => l !== null);
|
|
1844
|
+
if (legsBuilt.length === 0) return null;
|
|
1845
|
+
const [primary, ...extras] = legsBuilt;
|
|
1846
|
+
return {
|
|
1847
|
+
domain,
|
|
1848
|
+
legs: ["compile-valid", "standalone-provable"],
|
|
1849
|
+
...primary!,
|
|
1850
|
+
...(extras.length > 0 ? { extraCreates: extras } : {}),
|
|
1632
1851
|
};
|
|
1633
1852
|
}
|
|
1634
1853
|
|
|
@@ -1671,9 +1890,20 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
|
|
|
1671
1890
|
if (childBirthWithSetup !== undefined) legs.push("child-birth-provable");
|
|
1672
1891
|
if (relationBootstrap !== undefined) legs.push("relation-bootstrap-provable");
|
|
1673
1892
|
|
|
1893
|
+
// BIRTHS DX: every `.births()` directive either carries a discoverable leg (whose recipeIssues we
|
|
1894
|
+
// surface) or a REASON it has none — a birth is never silently unprovable.
|
|
1895
|
+
const birthDiagnostics: string[] = [];
|
|
1896
|
+
for (const d of mod.directives as Directive<unknown>[]) {
|
|
1897
|
+
if (d?.birthsChild !== true || typeof d.payloadSchema !== "object") continue;
|
|
1898
|
+
const r = discoverChildBirthLegOrReason(d, domain, modules);
|
|
1899
|
+
if (r.reason !== undefined) birthDiagnostics.push(r.reason);
|
|
1900
|
+
for (const issue of r.leg?.recipeIssues ?? []) birthDiagnostics.push(`${domain}/${d.id}: ${issue}`);
|
|
1901
|
+
}
|
|
1902
|
+
|
|
1674
1903
|
return {
|
|
1675
1904
|
domain,
|
|
1676
1905
|
legs,
|
|
1906
|
+
...(birthDiagnostics.length > 0 ? { birthDiagnostics } : {}),
|
|
1677
1907
|
...(standalone !== null
|
|
1678
1908
|
? {
|
|
1679
1909
|
directiveId: standalone.directiveId,
|
|
@@ -1683,6 +1913,8 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
|
|
|
1683
1913
|
...(standalone.mintField !== undefined ? { mintField: standalone.mintField } : {}),
|
|
1684
1914
|
...(standalone.query !== undefined ? { query: standalone.query } : {}),
|
|
1685
1915
|
...(standalone.count !== undefined ? { count: standalone.count } : {}),
|
|
1916
|
+
...(standalone.spatial !== undefined ? { spatial: standalone.spatial } : {}),
|
|
1917
|
+
...(standalone.extraCreates !== undefined ? { extraCreates: standalone.extraCreates } : {}),
|
|
1686
1918
|
}
|
|
1687
1919
|
: {}),
|
|
1688
1920
|
...(childBirthWithSetup !== undefined ? { childBirth: childBirthWithSetup } : {}),
|
package/src/codegen_ts.ts
CHANGED
|
@@ -40,6 +40,7 @@ import type { AggregateHandle } from "./aggregate.js";
|
|
|
40
40
|
import type { Field, ValueObjectDescriptor, ValueObjectFieldDescriptor } from "./fields.js";
|
|
41
41
|
import type { Directive } from "./directive.js";
|
|
42
42
|
import type { DomainModule } from "./codegen_dart.js";
|
|
43
|
+
import type { NomosCompat } from "./compat_manifest.js";
|
|
43
44
|
import type { QueryDecl } from "./query.js";
|
|
44
45
|
import { voMetaOf, collectPayloadValueObjectDescriptors } from "./value_object.js";
|
|
45
46
|
import { finishCount } from "./count.js";
|
|
@@ -68,6 +69,9 @@ export const tsHashConstName = (packageName: string): string => `${upperSnake(pa
|
|
|
68
69
|
/** `tool-library` → `TOOL_LIBRARY_INTERFACE` — the baked structural-interface const (#72). */
|
|
69
70
|
export const interfaceConstName = (packageName: string): string => `${upperSnake(packageName)}_INTERFACE`;
|
|
70
71
|
|
|
72
|
+
/** `tool-library` → `TOOL_LIBRARY_COMPAT` — the baked client/law compat manifest (§5). */
|
|
73
|
+
export const compatConstName = (packageName: string): string => `${upperSnake(packageName)}_COMPAT`;
|
|
74
|
+
|
|
71
75
|
// ── zod → TS (the same `_def ?? def` convention as codegen_dart's walker) ─────────
|
|
72
76
|
// The walkers are exported: codegen_proof.ts synthesizes SAMPLE payloads from the
|
|
73
77
|
// same zod shapes this file derives TYPES from — one introspection, two emitters.
|
|
@@ -379,6 +383,9 @@ export interface TsClientOptions {
|
|
|
379
383
|
* (read shapes, sid-keyed), keyed by domain. Baked in so `connect()` can negotiate
|
|
380
384
|
* shape-compatibility against the workspace's installed offer — NOT by hash. */
|
|
381
385
|
readonly interface?: Record<string, unknown>;
|
|
386
|
+
/** The CLIENT/LAW COMPAT MANIFEST (§5) — baked in so the factory can hand it to the
|
|
387
|
+
* holon's connect-time classifier (DIAGNOSIS only, never a gate). */
|
|
388
|
+
readonly compat?: NomosCompat;
|
|
382
389
|
}
|
|
383
390
|
|
|
384
391
|
/** TRUE when any module declares a PACKED workspace taxonomy (the routed-holon shape).
|
|
@@ -422,6 +429,16 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
422
429
|
``,
|
|
423
430
|
]
|
|
424
431
|
: []),
|
|
432
|
+
...(opts.compat !== undefined
|
|
433
|
+
? [
|
|
434
|
+
`/** The CLIENT/LAW COMPAT MANIFEST (§5): the law hash + structural interface hash + generator`,
|
|
435
|
+
` * provenance this client was generated against. A DIAGNOSIS surface — the factory hands it to`,
|
|
436
|
+
` * the holon's connect-time classifier, which logs a warning on a law-hash mismatch. It NEVER`,
|
|
437
|
+
` * gates: a hash mismatch with a compatible SHAPE is normal during law evolution (#72). */`,
|
|
438
|
+
`export const ${compatConstName(opts.packageName)} = ${JSON.stringify(opts.compat)} as const;`,
|
|
439
|
+
``,
|
|
440
|
+
]
|
|
441
|
+
: []),
|
|
425
442
|
`/** One projection row: the aggregate's wire type, its id, and the folded fields. */`,
|
|
426
443
|
`export interface Row<T> { type: string; id: string; data: T }`,
|
|
427
444
|
``,
|
|
@@ -438,6 +455,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
438
455
|
` watchById(aggregateId: string, cb: (rows: unknown) => void, opts?: { intervalMs?: number }): () => void;`,
|
|
439
456
|
` /** Declared-count / declared-sum O(1) reads (maintained by the read engine). */`,
|
|
440
457
|
` count(countId: string, groupKey?: string): Promise<number>;`,
|
|
458
|
+
` /** THE SPATIAL READ (R*Tree probe): rows intersecting the bbox — same row shape as query().`,
|
|
459
|
+
` * OPTIONAL member: structural binding tolerates a pre-spatial holon (the accessor then throws at call). */`,
|
|
460
|
+
` spatialWithin?(spatialId: string, bbox: { minLng: number; minLat: number; maxLng: number; maxLat: number }): Promise<unknown>;`,
|
|
441
461
|
` sum(sumId: string, groupKey?: string): Promise<number>;`,
|
|
442
462
|
...(anyPackedTaxonomy(modules)
|
|
443
463
|
? [
|
|
@@ -450,6 +470,10 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
450
470
|
` deadLetters(): Promise<{ id?: string; oid: string; source: string; error: string; domain?: string | null; directiveId?: string | null; at: string }[]>;`,
|
|
451
471
|
` retryDeadLetter(id: string): Promise<{ ok: boolean; head?: string; alreadyOnMain?: boolean; error?: string }>;`,
|
|
452
472
|
` discardDeadLetter(id: string): Promise<{ ok: boolean; removed: number }>;`,
|
|
473
|
+
` /** THE CONNECT-TIME COMPAT CLASSIFIER (§5) — OPTIONAL: a holon from @githolon/client's connect()`,
|
|
474
|
+
` * attaches it (implemented by \`checkCompat\`); the generated factory hands over this client's baked`,
|
|
475
|
+
` * compat manifest so a law-hash mismatch logs a warning. DIAGNOSIS only — never throws, never gates. */`,
|
|
476
|
+
` checkCompat?(compat: { lawHash: string; interfaceHash: string; packageName: string }): void;`,
|
|
453
477
|
`}`,
|
|
454
478
|
``,
|
|
455
479
|
`/** AUTHORITY IS UNFORGEABLE-BY-CONSTRUCTION: a GATED directive (one carrying a \`.requires(relation)\` write`,
|
|
@@ -824,6 +848,12 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
824
848
|
}
|
|
825
849
|
}
|
|
826
850
|
|
|
851
|
+
for (const sp of (mod.spatials ?? []) as { id: string; of: string; on: string }[]) {
|
|
852
|
+
methods.push(
|
|
853
|
+
` /** Declared spatial index \`${sp.id}\` — rows of \`${sp.of}\` whose \`${sp.on}\` geometry intersects the bbox (R*Tree probe). */`,
|
|
854
|
+
` ${lcFirst(camel(sp.id))}: (bbox: { minLng: number; minLat: number; maxLng: number; maxLat: number }) => (holon.spatialWithin ?? (() => { throw new Error("this holon predates spatial reads — upgrade @githolon/client"); }))(${JSON.stringify(sp.id)}, bbox) as Promise<Row<${sp.of}Data>[]>,`,
|
|
855
|
+
);
|
|
856
|
+
}
|
|
827
857
|
for (const q of (mod.queries ?? []) as QueryDecl[]) {
|
|
828
858
|
const agg = aggByType.get(q.returns);
|
|
829
859
|
const params = q.key
|
|
@@ -890,6 +920,9 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
890
920
|
`export function ${tsClientFactoryName(domain)}(holon: NomosHolon, opts: { domainHash?: string; actor?: string } = {}) {`,
|
|
891
921
|
` const domainHash = opts.domainHash ?? ${hashConst};`,
|
|
892
922
|
` const actor = opts.actor;`,
|
|
923
|
+
...(opts.compat !== undefined
|
|
924
|
+
? [` holon.checkCompat?.(${compatConstName(opts.packageName)});`]
|
|
925
|
+
: []),
|
|
893
926
|
` return {`,
|
|
894
927
|
` domain: ${JSON.stringify(domain)} as const,`,
|
|
895
928
|
` domainHash,`,
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
// The CLIENT/LAW COMPATIBILITY MANIFEST (§5, architecture/plug_and_play_dx.md).
|
|
2
|
+
//
|
|
3
|
+
// A DIAGNOSIS surface, NEVER a gate. Clients fit workspaces by STRUCTURAL INTERFACE
|
|
4
|
+
// (stable-id subtyping, #72), not by law-hash pin — the hash pin was a bug we removed.
|
|
5
|
+
// This block is baked into the generated TS + Dart clients and mirrored into the deploy
|
|
6
|
+
// JSON so a connect() can classify: lawHash-equal (silent) vs lawHash-differs-shape-compat
|
|
7
|
+
// (proceed, log) vs structurally-incompatible (warn). It is FRONTEND-ONLY — emitted like
|
|
8
|
+
// the dart codegen hints, it does NOT enter the USD IR / package bytes / domainHash.
|
|
9
|
+
export interface NomosCompat {
|
|
10
|
+
/** The package name (`cfg.name`). */
|
|
11
|
+
readonly packageName: string;
|
|
12
|
+
/** The deploy domainHash — sha256 of the package bytes (`policy:{hash}`). */
|
|
13
|
+
readonly lawHash: string;
|
|
14
|
+
/** sha256 over the CANONICAL serialization of the package structural interface. */
|
|
15
|
+
readonly interfaceHash: string;
|
|
16
|
+
/** Per domain key → its admission-identity hash (absent for identity-excluded domains). */
|
|
17
|
+
readonly domains: Record<string, { readonly identityHash?: string }>;
|
|
18
|
+
/** The generator provenance — the `@githolon/dsl` version + this manifest's schema. */
|
|
19
|
+
readonly generator: { readonly dsl: string; readonly schema: number };
|
|
20
|
+
}
|