@githolon/dsl 0.61.3 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.61.3",
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_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
@@ -121,6 +121,7 @@ export {
121
121
  recordObject,
122
122
  type Directive,
123
123
  type ReferentialMarker,
124
+ type SecondaryOutput,
124
125
  type PreconditionDescriptor,
125
126
  } from "./directive.js";
126
127
  // FIRST-CLASS WORKSPACE TYPES (sharding §10 RATIFIED, slice 1) live on the SUBPATH
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") {