@githolon/dsl 0.61.2 → 0.62.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/codegen_proof.ts +5 -6
- package/src/codegen_ts.ts +8 -1
- package/src/directive.ts +42 -0
- package/src/index.ts +1 -0
- package/src/manifest.ts +21 -0
- package/src/usd.ts +9 -0
- package/src/usd_layers.ts +11 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.62.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_proof.ts
CHANGED
|
@@ -2108,17 +2108,16 @@ export function legsForDomain(mod: DomainModule, modules: readonly DomainModule[
|
|
|
2108
2108
|
// only the first left the other children compile-valid-only (unprovable through their birth).
|
|
2109
2109
|
const parentBirths = allBirths.filter((b) => b.parentDomain === domain);
|
|
2110
2110
|
const childBirths = parentBirths.map((b) => {
|
|
2111
|
-
const withParent = attachParentSetup(b, allBirths);
|
|
2112
2111
|
// BIRTHS ISOLATION: with >1 birth leg, uniquify each leg's synthesized child so the legs don't collide
|
|
2113
2112
|
// on one generated child workspace name (the discriminator = the birth directive id, unique per leg).
|
|
2113
|
+
// MUTABLE leg — uniquify does NOT short-circuit the relationSetup attachment below (co2's two sibling
|
|
2114
|
+
// gated births both need BOTH: a distinct child AND the seeded authority chain).
|
|
2115
|
+
let withParent = attachParentSetup(b, allBirths);
|
|
2114
2116
|
if (parentBirths.length > 1) {
|
|
2115
2117
|
const birthDir = (mod.directives as Directive<unknown>[]).find((d) => d?.id === b.birthDirectiveId);
|
|
2116
2118
|
const schema = birthDir?.payloadSchema as z.ZodTypeAny | undefined;
|
|
2117
|
-
const
|
|
2118
|
-
|
|
2119
|
-
if (uniq !== withParent.birthPayload) {
|
|
2120
|
-
return { ...withParent, birthPayload: uniq };
|
|
2121
|
-
}
|
|
2119
|
+
const uniq = uniquifyBirthPayload(withParent.birthPayload, b.birthDirectiveId, schema);
|
|
2120
|
+
if (uniq !== withParent.birthPayload) withParent = { ...withParent, birthPayload: uniq };
|
|
2122
2121
|
}
|
|
2123
2122
|
// A GATED birth with no parent-birth authority path: seat its relation via the SAME granter/seed
|
|
2124
2123
|
// discovery the relation-bootstrap leg uses (the law's own authority path, e.g. co2's
|
package/src/codegen_ts.ts
CHANGED
|
@@ -1406,8 +1406,15 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
|
|
|
1406
1406
|
? ` const _sealed = await _sealFields(holon, keyStore, ${encConst}[${JSON.stringify(domain)}].directives[${JSON.stringify(d.id)}] as Record<string, { scope: string; aggSid: string; fieldSid: string }>, payload as Record<string, unknown>, callOpts?.seal !== false) as ${payloadName};`
|
|
1407
1407
|
: "";
|
|
1408
1408
|
const pv = wantSeal ? "_sealed" : "payload";
|
|
1409
|
+
// MULTI-OUTPUT: name every aggregate this directive writes (primary marker target + declared
|
|
1410
|
+
// secondaryOutputs), so the frontend dev sees "writes A + B" at the call site (not one by convention).
|
|
1411
|
+
const dd = d as unknown as { aggregateId: string; marker: string; secondaryOutputs?: readonly { aggregateId: string; marker: string }[] };
|
|
1412
|
+
const allOutputs = [{ aggregateId: dd.aggregateId, marker: dd.marker }, ...(dd.secondaryOutputs ?? [])];
|
|
1413
|
+
const writesClause = allOutputs.length > 1
|
|
1414
|
+
? ` Writes ${allOutputs.map((o) => `\`${o.aggregateId}\` (${o.marker})`).join(" + ")}.`
|
|
1415
|
+
: "";
|
|
1409
1416
|
out.push(
|
|
1410
|
-
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at offer.` : ""}${mint ? ` Omitted \`${mint.field}\` is KERNEL-MINTED at offer (returned on the result).` : ""}${placed.length ? " Omitted `shard` is picked LEAST-LOADED at offer (the routed holon)." : ""} */`,
|
|
1417
|
+
`/** \`${domain}/${d.id}\` payload (mirrors the engine's zod schema).${writesClause}${stamps.length ? ` Omitted ${stamps.map((s) => `\`${s}\``).join("/")} auto-stamp(s) ISO now() at offer.` : ""}${mint ? ` Omitted \`${mint.field}\` is KERNEL-MINTED at offer (returned on the result).` : ""}${placed.length ? " Omitted `shard` is picked LEAST-LOADED at offer (the routed holon)." : ""} */`,
|
|
1411
1418
|
`export type ${payloadName} = ${tsTypeOfPayload(schema, stamps, mint ? [mint.field] : [], placed, lazyReg, d.id)};`,
|
|
1412
1419
|
``,
|
|
1413
1420
|
);
|
package/src/directive.ts
CHANGED
|
@@ -74,10 +74,26 @@ export interface PreconditionDescriptor {
|
|
|
74
74
|
* the intent's HLC against exactly this scope.
|
|
75
75
|
*/
|
|
76
76
|
|
|
77
|
+
/** One additional aggregate a directive DECLARES it writes (a multi-output directive), beyond its primary
|
|
78
|
+
* `.creates/.mutates/.ensures/.archives` target. Descriptive metadata (docs/proof/client), NOT a kernel
|
|
79
|
+
* write-restriction — the plan's ops are what fold. */
|
|
80
|
+
export interface SecondaryOutput {
|
|
81
|
+
readonly marker: ReferentialMarker;
|
|
82
|
+
readonly aggregateId: string;
|
|
83
|
+
}
|
|
84
|
+
|
|
77
85
|
export interface Directive<P = unknown> {
|
|
78
86
|
readonly id: string;
|
|
79
87
|
readonly marker: ReferentialMarker;
|
|
80
88
|
readonly aggregateId: string;
|
|
89
|
+
/**
|
|
90
|
+
* MULTI-OUTPUT DIRECTIVES: additional aggregates this directive declares it writes, beyond the primary
|
|
91
|
+
* marker target. Present iff the directive chained `.alsoEnsures/.alsoCreates/.alsoMutates/.alsoArchives`
|
|
92
|
+
* (or `.alsoWrites`). Purely additive metadata — generated docs/proof/client say "writes A + B" instead
|
|
93
|
+
* of picking one by convention; the kernel folds by the plan's ops regardless. Omit-when-empty (era-safe:
|
|
94
|
+
* a single-output directive is byte-identical in the IR to before this existed).
|
|
95
|
+
*/
|
|
96
|
+
readonly secondaryOutputs?: readonly SecondaryOutput[];
|
|
81
97
|
readonly payloadSchema: z.ZodType<P>;
|
|
82
98
|
readonly plan: (payload: P, ctx: Ports) => PlannedOp[];
|
|
83
99
|
/**
|
|
@@ -300,6 +316,18 @@ export interface Directive<P = unknown> {
|
|
|
300
316
|
* each `plan` — the kernel resolves it WITHIN the one shared `admit` gate.
|
|
301
317
|
*/
|
|
302
318
|
export interface RequirableDirective<P> extends Directive<P> {
|
|
319
|
+
/**
|
|
320
|
+
* MULTI-OUTPUT: declare an ADDITIONAL aggregate this directive writes (beyond its primary marker target),
|
|
321
|
+
* so generated docs/proof/client metadata name every output ("writes A + B") instead of one by convention.
|
|
322
|
+
* `.alsoEnsures/.alsoCreates/.alsoMutates/.alsoArchives` are the marker-specific sugar; `.alsoWrites` takes
|
|
323
|
+
* an explicit marker. Chainable + additive (a directive that never calls these is byte-identical). The
|
|
324
|
+
* kernel folds by the plan's ops regardless — this is descriptive law metadata, not a write-restriction.
|
|
325
|
+
*/
|
|
326
|
+
alsoWrites(aggregate: AggregateHandle, marker: ReferentialMarker): RequirableDirective<P>;
|
|
327
|
+
alsoEnsures(aggregate: AggregateHandle): RequirableDirective<P>;
|
|
328
|
+
alsoCreates(aggregate: AggregateHandle): RequirableDirective<P>;
|
|
329
|
+
alsoMutates(aggregate: AggregateHandle): RequirableDirective<P>;
|
|
330
|
+
alsoArchives(aggregate: AggregateHandle): RequirableDirective<P>;
|
|
303
331
|
/**
|
|
304
332
|
* Declare the authz pre-condition: the `capability` the actor must hold (defaults
|
|
305
333
|
* to the directive id) at the scope `scopeFrom(payload)` derives (defaults to root).
|
|
@@ -636,8 +664,22 @@ export function recordObject(
|
|
|
636
664
|
* declaration site reads `directive(id)…plan(fn).requires("cap", p => […])`.
|
|
637
665
|
*/
|
|
638
666
|
function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
667
|
+
const alsoWrites = (aggregate: AggregateHandle, marker: ReferentialMarker): RequirableDirective<P> => {
|
|
668
|
+
// Dedup by (marker, aggregateId) + the primary target — declaring the same output twice is a no-op, and
|
|
669
|
+
// re-declaring the primary aggregate is ignored (it is already the marker target). Sorted at emit time.
|
|
670
|
+
const next: SecondaryOutput = { marker, aggregateId: aggregate.id };
|
|
671
|
+
const isPrimary = aggregate.id === base.aggregateId;
|
|
672
|
+
const already = (base.secondaryOutputs ?? []).some((s) => s.marker === marker && s.aggregateId === aggregate.id);
|
|
673
|
+
if (isPrimary || already) return makeRequirable(base);
|
|
674
|
+
return makeRequirable({ ...base, secondaryOutputs: [...(base.secondaryOutputs ?? []), next] });
|
|
675
|
+
};
|
|
639
676
|
return {
|
|
640
677
|
...base,
|
|
678
|
+
alsoWrites,
|
|
679
|
+
alsoEnsures: (aggregate: AggregateHandle) => alsoWrites(aggregate, "ensures"),
|
|
680
|
+
alsoCreates: (aggregate: AggregateHandle) => alsoWrites(aggregate, "creates"),
|
|
681
|
+
alsoMutates: (aggregate: AggregateHandle) => alsoWrites(aggregate, "mutates"),
|
|
682
|
+
alsoArchives: (aggregate: AggregateHandle) => alsoWrites(aggregate, "archives"),
|
|
641
683
|
requires(
|
|
642
684
|
relation: string,
|
|
643
685
|
opts?: { object?: string; objectFrom?: { recordType: string; idField: string } },
|
package/src/index.ts
CHANGED
package/src/manifest.ts
CHANGED
|
@@ -29,6 +29,7 @@ import { createHash } from "node:crypto";
|
|
|
29
29
|
import { z } from "zod";
|
|
30
30
|
import { encodeKernelSchema, encodeRefMode } from "./wire_encode.js";
|
|
31
31
|
import type { WireSchema, WireRefMode } from "./wire.js";
|
|
32
|
+
import type { ReferentialMarker } from "./directive.js";
|
|
32
33
|
import type { DomainModule } from "./codegen_dart.js";
|
|
33
34
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
34
35
|
import type { QueryDecl } from "./query.js";
|
|
@@ -114,6 +115,13 @@ export interface CanonicalDirective {
|
|
|
114
115
|
/** The target aggregate id. */
|
|
115
116
|
readonly target: string;
|
|
116
117
|
readonly marker: WireRefMode;
|
|
118
|
+
/**
|
|
119
|
+
* MULTI-OUTPUT DIRECTIVES: additional aggregates this directive declares it writes, beyond the primary
|
|
120
|
+
* `target`/`marker` — `{ target, marker }` pairs SORTED by (target, marker). Descriptive law metadata
|
|
121
|
+
* (docs/proof/client name every output). OMITTED ENTIRELY when the directive declares none, so a
|
|
122
|
+
* single-output directive is byte-identical to before this key existed (pinned golden hashes unchanged).
|
|
123
|
+
*/
|
|
124
|
+
readonly secondaryOutputs?: readonly { target: string; marker: WireRefMode }[];
|
|
117
125
|
/** The payload's top-level object shape as SORTED `[name, type]` pairs. */
|
|
118
126
|
readonly payload: CanonicalPayloadField[];
|
|
119
127
|
/**
|
|
@@ -645,6 +653,18 @@ function canonicalReads(declared: string[]): { reads?: string[] } {
|
|
|
645
653
|
return { reads: [...declared].sort() };
|
|
646
654
|
}
|
|
647
655
|
|
|
656
|
+
/** Canonicalize a directive's MULTI-OUTPUT secondary writes → `{ target, marker }` sorted by (target,
|
|
657
|
+
* marker). Omit-when-empty (the era rule — a single-output directive contributes no key). */
|
|
658
|
+
function canonicalSecondaryOutputs(
|
|
659
|
+
declared: readonly { marker: ReferentialMarker; aggregateId: string }[] | undefined,
|
|
660
|
+
): { secondaryOutputs?: { target: string; marker: WireRefMode }[] } {
|
|
661
|
+
if (declared === undefined || declared.length === 0) return {};
|
|
662
|
+
const out = declared
|
|
663
|
+
.map((s) => ({ target: s.aggregateId, marker: encodeRefMode(s.marker) }))
|
|
664
|
+
.sort((a, b) => (a.target !== b.target ? (a.target < b.target ? -1 : 1) : a.marker < b.marker ? -1 : a.marker > b.marker ? 1 : 0));
|
|
665
|
+
return { secondaryOutputs: out };
|
|
666
|
+
}
|
|
667
|
+
|
|
648
668
|
/**
|
|
649
669
|
* Canonicalize a directive's declared required HOST CAPABILITY ports into a manifest fragment
|
|
650
670
|
* (the HOST/PORT axis, invariant 3). OMIT-WHEN-EMPTY: returns `{}` (no `requiresHostCapability`
|
|
@@ -1046,6 +1066,7 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
1046
1066
|
id: d.id,
|
|
1047
1067
|
target: d.aggregateId,
|
|
1048
1068
|
marker: encodeRefMode(d.marker),
|
|
1069
|
+
...canonicalSecondaryOutputs(d.secondaryOutputs),
|
|
1049
1070
|
payload: canonicalPayload(d.payloadSchema),
|
|
1050
1071
|
payloadJsonSchema: canonicalPayloadJsonSchema(d.payloadSchema),
|
|
1051
1072
|
// Omit-when-empty: a directive declaring NO boundary contributes neither key,
|
package/src/usd.ts
CHANGED
|
@@ -106,6 +106,12 @@ export type UsdPrim =
|
|
|
106
106
|
* (`undefined`) when no emits are declared (omit-when-empty hash invariance).
|
|
107
107
|
*/
|
|
108
108
|
readonly emits?: Record<string, { max?: number }>;
|
|
109
|
+
/**
|
|
110
|
+
* MULTI-OUTPUT: the ADDITIONAL aggregates this directive declares it writes (beyond `target`/`marker`),
|
|
111
|
+
* `{ target, marker }` sorted. OMITTED (`undefined`) for a single-output directive (omit-when-empty
|
|
112
|
+
* hash invariance). Descriptive law metadata — docs/proof/client name every output.
|
|
113
|
+
*/
|
|
114
|
+
readonly secondaryOutputs?: readonly { target: string; marker: WireRefMode }[];
|
|
109
115
|
/**
|
|
110
116
|
* Standard JSON Schema for the directive payload, emitted from TS/Zod into the
|
|
111
117
|
* IR so cross-language tooling validates against the same schema source.
|
|
@@ -664,6 +670,9 @@ function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): Us
|
|
|
664
670
|
// directive declared them, so a boundary-free prim is byte-identical to before.
|
|
665
671
|
...(d.reads !== undefined ? { reads: d.reads } : {}),
|
|
666
672
|
...(d.emits !== undefined ? { emits: d.emits } : {}),
|
|
673
|
+
// MULTI-OUTPUT: the additional aggregates this directive declares it writes (self-describing law).
|
|
674
|
+
// Omit-when-empty from the manifest, so a single-output prim is byte-identical to before.
|
|
675
|
+
...(d.secondaryOutputs !== undefined ? { secondaryOutputs: d.secondaryOutputs } : {}),
|
|
667
676
|
payloadJsonSchema: d.payloadJsonSchema,
|
|
668
677
|
// `scope`/`payloadRef` deliberately omitted (undefined): no static scope in
|
|
669
678
|
// the DSL today, payload is the stage-2 certified artifact.
|
package/src/usd_layers.ts
CHANGED
|
@@ -349,6 +349,11 @@ export function usdaLayerText(
|
|
|
349
349
|
`${ind3}custom string nomos:emitsHex = ${usdaString(hexUtf8(JSON.stringify(prim.emits)))}`,
|
|
350
350
|
);
|
|
351
351
|
}
|
|
352
|
+
if (prim.secondaryOutputs !== undefined) {
|
|
353
|
+
body.push(
|
|
354
|
+
`${ind3}custom string nomos:secondaryOutputsHex = ${usdaString(hexUtf8(JSON.stringify(prim.secondaryOutputs)))}`,
|
|
355
|
+
);
|
|
356
|
+
}
|
|
352
357
|
if (prim.payloadJsonSchema !== undefined) {
|
|
353
358
|
body.push(
|
|
354
359
|
`${ind3}custom string nomos:payloadJsonSchemaHex = ${usdaString(
|
|
@@ -503,6 +508,7 @@ function foldOpinionPrim(base: UsdPrim, over: UsdPrim): UsdPrim {
|
|
|
503
508
|
// flattened bytes match `encodeModuleToPrims` exactly.
|
|
504
509
|
const reads = over.reads ?? base.reads;
|
|
505
510
|
const emits = over.emits ?? base.emits;
|
|
511
|
+
const secondaryOutputs = over.secondaryOutputs ?? base.secondaryOutputs;
|
|
506
512
|
const payloadJsonSchema = over.payloadJsonSchema ?? base.payloadJsonSchema;
|
|
507
513
|
return {
|
|
508
514
|
kind: "directive",
|
|
@@ -512,6 +518,7 @@ function foldOpinionPrim(base: UsdPrim, over: UsdPrim): UsdPrim {
|
|
|
512
518
|
requires: over.requires,
|
|
513
519
|
...(reads !== undefined ? { reads } : {}),
|
|
514
520
|
...(emits !== undefined ? { emits } : {}),
|
|
521
|
+
...(secondaryOutputs !== undefined ? { secondaryOutputs } : {}),
|
|
515
522
|
...(payloadJsonSchema !== undefined ? { payloadJsonSchema } : {}),
|
|
516
523
|
};
|
|
517
524
|
}
|
|
@@ -1244,6 +1251,9 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1244
1251
|
const emits = p.attrs.has("nomos:emitsHex")
|
|
1245
1252
|
? (JSON.parse(unhexUtf8(attrString(p, "nomos:emitsHex"))) as Record<string, { max?: number }>)
|
|
1246
1253
|
: undefined;
|
|
1254
|
+
const secondaryOutputs = p.attrs.has("nomos:secondaryOutputsHex")
|
|
1255
|
+
? (JSON.parse(unhexUtf8(attrString(p, "nomos:secondaryOutputsHex"))) as { target: string; marker: WireRefMode }[])
|
|
1256
|
+
: undefined;
|
|
1247
1257
|
const payloadJsonSchema = p.attrs.has("nomos:payloadJsonSchemaHex")
|
|
1248
1258
|
? (JSON.parse(unhexUtf8(attrString(p, "nomos:payloadJsonSchemaHex"))) as unknown)
|
|
1249
1259
|
: undefined;
|
|
@@ -1255,6 +1265,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1255
1265
|
requires: attrStringArray(p, "nomos:requires"),
|
|
1256
1266
|
...(reads !== undefined ? { reads } : {}),
|
|
1257
1267
|
...(emits !== undefined ? { emits } : {}),
|
|
1268
|
+
...(secondaryOutputs !== undefined ? { secondaryOutputs } : {}),
|
|
1258
1269
|
...(payloadJsonSchema !== undefined ? { payloadJsonSchema } : {}),
|
|
1259
1270
|
});
|
|
1260
1271
|
} else if (kind === "query") {
|