@githolon/dsl 0.1.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/LICENSE.md +36 -0
- package/compile_package.mjs +50 -0
- package/package.json +59 -0
- package/src/aggregate.ts +167 -0
- package/src/authoring.ts +119 -0
- package/src/build_package.ts +636 -0
- package/src/certified_read.ts +313 -0
- package/src/codegen_dart.ts +2732 -0
- package/src/codegen_dot.ts +466 -0
- package/src/codegen_provider_dart.ts +358 -0
- package/src/codegen_ts.ts +365 -0
- package/src/codegen_usda.ts +388 -0
- package/src/combined.ts +195 -0
- package/src/compile_engine.ts +567 -0
- package/src/compile_package_main.ts +496 -0
- package/src/compose.ts +317 -0
- package/src/count.ts +218 -0
- package/src/ctx.ts +57 -0
- package/src/derived.ts +138 -0
- package/src/directive.ts +306 -0
- package/src/drivers.ts +95 -0
- package/src/emits_guard.ts +123 -0
- package/src/engine_entry.ts +449 -0
- package/src/exists.ts +170 -0
- package/src/extremum.ts +227 -0
- package/src/fields.ts +291 -0
- package/src/framework/bootstrap.ts +22 -0
- package/src/framework/disclosure.ts +108 -0
- package/src/framework/domain_lifecycle.ts +108 -0
- package/src/framework/identity.ts +537 -0
- package/src/framework/impure_capability.ts +643 -0
- package/src/framework/rbac.ts +418 -0
- package/src/framework/repair.ts +150 -0
- package/src/framework/sync_lifecycle.ts +125 -0
- package/src/framework/workspace_invariant.ts +128 -0
- package/src/framework/workspaces.ts +817 -0
- package/src/index.ts +317 -0
- package/src/manifest.ts +947 -0
- package/src/ops.ts +145 -0
- package/src/ordered_read.ts +228 -0
- package/src/predicate.ts +203 -0
- package/src/query/compile.ts +0 -0
- package/src/query/relations.ts +144 -0
- package/src/query.ts +151 -0
- package/src/read.ts +54 -0
- package/src/relation.ts +189 -0
- package/src/report/csv.ts +54 -0
- package/src/report.ts +401 -0
- package/src/spatial.ts +115 -0
- package/src/sum.ts +194 -0
- package/src/usd.ts +563 -0
- package/src/wire.ts +149 -0
- package/src/wire_encode.ts +250 -0
package/src/wire.ts
ADDED
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
|
|
5
|
+
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* The kernel's wire shapes — byte-compatible with serde_json's externally-tagged
|
|
10
|
+
* enum encoding. These types mirror `nomos2/kernel/src/lib.rs` EXACTLY; the shape
|
|
11
|
+
* was discovered empirically (see FINDINGS.md) by serializing real kernel values.
|
|
12
|
+
*
|
|
13
|
+
* serde externally-tags enums:
|
|
14
|
+
* - unit variant -> bare string: Driver::Lww => "Lww"
|
|
15
|
+
* - newtype variant -> {Tag: inner}: Value::Int(10) => {"Int":10}
|
|
16
|
+
* - struct variant -> {Tag: {..}}: Op::SetEntry{key,value}=> {"SetEntry":{"key":..,"value":..}}
|
|
17
|
+
* - newtype struct -> transparent: ReplicaId(7) => 7
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
/** `ReplicaId(pub u64)` serializes as a bare number. */
|
|
21
|
+
export type WireReplicaId = number;
|
|
22
|
+
|
|
23
|
+
/** `Hlc { physical: u64, logical: u32, replica: ReplicaId }`. */
|
|
24
|
+
export interface WireHlc {
|
|
25
|
+
physical: number;
|
|
26
|
+
logical: number;
|
|
27
|
+
replica: WireReplicaId;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** `Value = Str | Int | Set | Map | Evidence`. (Map only appears in *state*, never in an
|
|
31
|
+
* Op input here.) `Evidence` is the content-addressed EvidenceRef (evidence.md §1/§9.2):
|
|
32
|
+
* a struct-bearing newtype variant `{Evidence: {hash, media_type, byte_length,
|
|
33
|
+
* storage_class}}`, mirroring serde's externally-tagged encoding of
|
|
34
|
+
* `Value::Evidence(EvidenceRef)`. The field names are the kernel's SNAKE_CASE serde
|
|
35
|
+
* fields (`media_type`/`byte_length`/`storage_class`), and `storage_class` is the serde
|
|
36
|
+
* unit-variant bare string `"Git" | "External"`. */
|
|
37
|
+
export type WireValue =
|
|
38
|
+
| { Str: string }
|
|
39
|
+
| { Int: number }
|
|
40
|
+
| { Set: string[] }
|
|
41
|
+
| { Map: Record<string, WireField> }
|
|
42
|
+
| { Evidence: WireEvidenceRef };
|
|
43
|
+
|
|
44
|
+
/** `EvidenceRef { hash: ContentHash, media_type, byte_length, storage_class }` — the
|
|
45
|
+
* kernel serde shape. `ContentHash(String)` serializes transparently as a bare string
|
|
46
|
+
* (a newtype struct), and `StorageClass` as a bare unit-variant string. */
|
|
47
|
+
export interface WireEvidenceRef {
|
|
48
|
+
hash: string;
|
|
49
|
+
media_type: string;
|
|
50
|
+
byte_length: number;
|
|
51
|
+
storage_class: "Git" | "External";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `Field { value: Value, stamp: Hlc }` — only used when reading state back. */
|
|
55
|
+
export interface WireField {
|
|
56
|
+
value: WireValue;
|
|
57
|
+
stamp: WireHlc;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** `Op = Set(Value) | AddToSet(BTreeSet<String>) | SetEntry { key, value }`. */
|
|
61
|
+
export type WireOp =
|
|
62
|
+
| { Set: WireValue }
|
|
63
|
+
| { AddToSet: string[] }
|
|
64
|
+
| { SetEntry: { key: string; value: WireValue } };
|
|
65
|
+
|
|
66
|
+
/** `FieldOp { field: FieldId, op: Op }`. */
|
|
67
|
+
export interface WireFieldOp {
|
|
68
|
+
field: string;
|
|
69
|
+
op: WireOp;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* `RefMode = Create | Mutate | Ensure | Archive` — the referential intent of an
|
|
74
|
+
* event's touch on its aggregate (serde unit-variant: a bare string). The DSL's
|
|
75
|
+
* `.creates`/`.mutates`/`.ensures`/`.archives` markers lower to these; the
|
|
76
|
+
* kernel's referential guard enforces them at write time. `serde(default)` is
|
|
77
|
+
* `Ensure`, so older unmarked wire stays permissive.
|
|
78
|
+
*/
|
|
79
|
+
export type WireRefMode = "Create" | "Mutate" | "Ensure" | "Archive";
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* `Event { aggregate: AggregateId, marker: RefMode, ops: Vec<FieldOp> }` — one
|
|
83
|
+
* aggregate's mutations (the *where* + the field-ops) plus its referential
|
|
84
|
+
* marker. A directive that touches N aggregates emits N events (kernel
|
|
85
|
+
* #56, aggregate identity). `marker` is `serde(default)` = "Ensure".
|
|
86
|
+
*/
|
|
87
|
+
export interface WireEvent {
|
|
88
|
+
aggregate: string;
|
|
89
|
+
marker: WireRefMode;
|
|
90
|
+
ops: WireFieldOp[];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* `Intent { id: IntentId, hlc: Hlc, strikes: Vec<IntentId>, events: Vec<Event> }`
|
|
95
|
+
* — one intent = one commit = the atomic unit. Carries the encoded event bundle: many
|
|
96
|
+
* per-aggregate events, all sharing the intent's HLC, all-or-nothing. This is the
|
|
97
|
+
* blob written as `intent.json`.
|
|
98
|
+
*
|
|
99
|
+
* `id` identifies the commit; `strikes` lists intent ids this intent retracts
|
|
100
|
+
* (the strike-out / revert hatch). Both are `serde(default)` on the kernel, so
|
|
101
|
+
* older wire payloads still deserialize.
|
|
102
|
+
*
|
|
103
|
+
* `reads` is the captured READ FOOTPRINT: the O(1) DSL-defined queries the plan read to justify
|
|
104
|
+
* this write, with their results, all taken at this intent's `hlc`. Replay serves each read from
|
|
105
|
+
* this record (never a live re-query) — determinism; and the gate can re-check a result against the
|
|
106
|
+
* projection at `hlc` to detect a stale premise — read/write conflict detection. OMITTED when empty
|
|
107
|
+
* so a read-free intent is byte-identical to before reads existed (`serde(default)`).
|
|
108
|
+
*/
|
|
109
|
+
export interface WireIntent {
|
|
110
|
+
id: string;
|
|
111
|
+
hlc: WireHlc;
|
|
112
|
+
strikes: string[];
|
|
113
|
+
events: WireEvent[];
|
|
114
|
+
reads?: WireRead[];
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* One captured O(1) DSL-query read on the write path (`aggregate_lifecycle_and_relations.md`). The
|
|
119
|
+
* plan reads through a declared, INDEXED query (never a scan); the engine records what was read and
|
|
120
|
+
* what came back, so the write's premise is committed, replayable, and auditable.
|
|
121
|
+
*/
|
|
122
|
+
export interface WireRead {
|
|
123
|
+
/** The O(1) DSL-defined query id (a managed index — e.g. the `t.hasMany` inverse). */
|
|
124
|
+
queryId: string;
|
|
125
|
+
/** The index key args the query was read with (key field -> value). */
|
|
126
|
+
args: Record<string, string>;
|
|
127
|
+
/** The captured result. Replay returns this verbatim; the gate may re-verify it against `hlc`. */
|
|
128
|
+
result: unknown;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* `Driver` — the kernel's externally-tagged serde encoding (`nomos2/kernel/src/lib.rs`).
|
|
133
|
+
* Unit variants → bare strings; `MapOf` → `{ MapOf: <inner> }`. This mirrors the kernel
|
|
134
|
+
* variants the DSL `encodeDriver` emits: the four originals plus the structural CRDT unit
|
|
135
|
+
* drivers `RemoveWins` / `Counter` / `LastPosition` (each a real `merge_field` arm).
|
|
136
|
+
* (`NumericMax`/`NumericMin`/`ImmutableAfterCreate`/`OrderedList` are kernel variants no
|
|
137
|
+
* DSL driver encodes to yet — added here only as the DSL gains a constant for them.)
|
|
138
|
+
*/
|
|
139
|
+
export type WireDriver =
|
|
140
|
+
| "Lww"
|
|
141
|
+
| "AddWins"
|
|
142
|
+
| "Conflict"
|
|
143
|
+
| "RemoveWins"
|
|
144
|
+
| "Counter"
|
|
145
|
+
| "LastPosition"
|
|
146
|
+
| { MapOf: WireDriver };
|
|
147
|
+
|
|
148
|
+
/** `Schema = BTreeMap<FieldId, Driver>` — a plain object, field -> Driver. */
|
|
149
|
+
export type WireSchema = Record<string, WireDriver>;
|
|
@@ -0,0 +1,250 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine} on V8 + WASI-shim, byte-identical everywhere. V8 = portability; the one
|
|
5
|
+
// wasm = determinism. No native, no wasmtime, no 2nd artifact, no domain-JS on bare V8.
|
|
6
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Deterministic directive execution and kernel wire encoding.
|
|
10
|
+
*
|
|
11
|
+
* - `encodeKernelSchema(agg)` -> the kernel `Schema` JSON (field -> Driver) for one aggregate.
|
|
12
|
+
* - `executeDirectiveToIntent(...)` -> a kernel-shaped `Intent` from a directive's
|
|
13
|
+
* `plan(payload, ctx)`.
|
|
14
|
+
*
|
|
15
|
+
* #56 (aggregate identity): a directive's `plan` produces `PlannedOp`s that each
|
|
16
|
+
* carry their target `aggregateId`. `executeDirectiveToIntent` groups those ops by aggregate
|
|
17
|
+
* (in first-seen / authored order) into per-aggregate `Event`s and emits one
|
|
18
|
+
* `Intent{hlc, events}` — the atomic commit. A single-aggregate directive yields
|
|
19
|
+
* one event; a multi-aggregate one yields several. Proto/numeric-tag wire is a
|
|
20
|
+
* LATER coordinated migration — we target the current serde_json shape now.
|
|
21
|
+
*/
|
|
22
|
+
import { encodeDriver } from "./drivers.js";
|
|
23
|
+
import { __resetAuthoring, __drainAuthoring } from "./authoring.js";
|
|
24
|
+
import { __resetReads, __drainReads } from "./read.js";
|
|
25
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
26
|
+
import type { Field, EvidenceRefValue } from "./fields.js";
|
|
27
|
+
import type { Directive, ReferentialMarker } from "./directive.js";
|
|
28
|
+
import type { PlannedOp, FieldOp } from "./ops.js";
|
|
29
|
+
import type { Ports } from "./ctx.js";
|
|
30
|
+
import type {
|
|
31
|
+
WireEvent,
|
|
32
|
+
WireHlc,
|
|
33
|
+
WireIntent,
|
|
34
|
+
WireFieldOp,
|
|
35
|
+
WireOp,
|
|
36
|
+
WireRefMode,
|
|
37
|
+
WireSchema,
|
|
38
|
+
WireValue,
|
|
39
|
+
} from "./wire.js";
|
|
40
|
+
|
|
41
|
+
/** Map a directive's referential marker onto the kernel `RefMode` wire string. */
|
|
42
|
+
export function encodeRefMode(marker: ReferentialMarker): WireRefMode {
|
|
43
|
+
switch (marker) {
|
|
44
|
+
case "creates":
|
|
45
|
+
return "Create";
|
|
46
|
+
case "mutates":
|
|
47
|
+
return "Mutate";
|
|
48
|
+
case "ensures":
|
|
49
|
+
return "Ensure";
|
|
50
|
+
case "archives":
|
|
51
|
+
return "Archive";
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** Derive a deterministic intent id from its HLC (replayable across TS/Dart). */
|
|
56
|
+
export function intentIdFromHlc(hlc: WireHlc): string {
|
|
57
|
+
return `${hlc.physical}.${hlc.logical}.${hlc.replica}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Encode one aggregate's fields as a kernel `Schema` (field id -> Driver). */
|
|
61
|
+
export function encodeKernelSchema(agg: AggregateHandle): WireSchema {
|
|
62
|
+
const out: WireSchema = {};
|
|
63
|
+
// `agg.fields` is STORED fields only (virtual `t.hasMany` inverses live in `agg.hasMany`),
|
|
64
|
+
// so every member here belongs in the wire schema — there is no virtual kind to skip.
|
|
65
|
+
for (const [name, field] of Object.entries(agg.fields as Record<string, Field>)) {
|
|
66
|
+
out[name] = encodeDriver(field.driver);
|
|
67
|
+
}
|
|
68
|
+
return out;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** Map an authored op value onto the kernel `Value` shape, given the field kind. */
|
|
72
|
+
function encodeKernelValue(field: Field | undefined, value: unknown): WireValue {
|
|
73
|
+
// An `evidence` field encodes as the FIRST-CLASS kernel `Value::Evidence` (NOT a JSON
|
|
74
|
+
// string): the authored {hash, mediaType, byteLength, storageClass} maps to the
|
|
75
|
+
// kernel's snake_case serde shape + the `Git`/`External` unit-variant. Checked BEFORE
|
|
76
|
+
// the scalar fallthroughs (an evidence value is an object, never a Str/Int).
|
|
77
|
+
if (field?.kind === "evidence") {
|
|
78
|
+
const v = value as EvidenceRefValue;
|
|
79
|
+
return {
|
|
80
|
+
Evidence: {
|
|
81
|
+
hash: v.hash,
|
|
82
|
+
media_type: v.mediaType,
|
|
83
|
+
byte_length: v.byteLength,
|
|
84
|
+
storage_class: v.storageClass === "external" ? "External" : "Git",
|
|
85
|
+
},
|
|
86
|
+
};
|
|
87
|
+
}
|
|
88
|
+
// For scalar Set/SetEntry the kernel expects Str or Int. Maps store leaves;
|
|
89
|
+
// a map's entry value is a leaf scalar.
|
|
90
|
+
if (typeof value === "number" && Number.isInteger(value)) return { Int: value };
|
|
91
|
+
if (typeof value === "string") return { Str: value };
|
|
92
|
+
if (typeof value === "boolean") return { Str: String(value) };
|
|
93
|
+
// Enum values are strings; refs are id strings — both handled above.
|
|
94
|
+
// Anything else (object/array as a scalar) is JSON-encoded into a Str leaf.
|
|
95
|
+
if (field?.kind === "json") return { Str: JSON.stringify(value) };
|
|
96
|
+
throw new Error(
|
|
97
|
+
`Cannot encode value ${JSON.stringify(value)} for field '${field?.kind ?? "?"}': ` +
|
|
98
|
+
`kernel scalars are Str | Int only.`,
|
|
99
|
+
);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function encodeKernelFieldOp(op: FieldOp, field: Field | undefined): WireFieldOp {
|
|
103
|
+
let wireOp: WireOp;
|
|
104
|
+
switch (op.kind) {
|
|
105
|
+
case "set":
|
|
106
|
+
wireOp = { Set: encodeKernelValue(field, op.value) };
|
|
107
|
+
break;
|
|
108
|
+
case "addToSet":
|
|
109
|
+
wireOp = { AddToSet: [...(op.items ?? [])] };
|
|
110
|
+
break;
|
|
111
|
+
case "setEntry":
|
|
112
|
+
wireOp = {
|
|
113
|
+
SetEntry: { key: op.entryKey!, value: encodeKernelValue(field, op.value) },
|
|
114
|
+
};
|
|
115
|
+
break;
|
|
116
|
+
}
|
|
117
|
+
return { field: op.field, op: wireOp };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Run a directive: validate payload (Zod), call `plan`, group the resulting ops
|
|
122
|
+
* by aggregate (in first-seen / authored order) into per-aggregate `Event`s, and
|
|
123
|
+
* emit one kernel `Intent{hlc, events}` — the atomic, all-or-nothing commit (#56).
|
|
124
|
+
*
|
|
125
|
+
* A single-aggregate directive yields one event; a directive that touches several
|
|
126
|
+
* aggregates yields several events. The events keep authored order, so an aggregate created by an earlier
|
|
127
|
+
* event is visible to later events in the same intent (kernel in-intent apply).
|
|
128
|
+
*
|
|
129
|
+
* `agg` is the directive's declared target handle, used to resolve field kinds
|
|
130
|
+
* when encoding values; ops emitted to sibling aggregates encode by value-kind.
|
|
131
|
+
*/
|
|
132
|
+
export function executeDirectiveToIntent<P>(
|
|
133
|
+
directive: Directive<P>,
|
|
134
|
+
agg: AggregateHandle,
|
|
135
|
+
payload: NoInfer<P>,
|
|
136
|
+
ctx: Ports,
|
|
137
|
+
): WireIntent {
|
|
138
|
+
if (directive.aggregateId !== agg.id) {
|
|
139
|
+
throw new Error(
|
|
140
|
+
`Directive '${directive.id}' targets aggregate '${directive.aggregateId}' ` +
|
|
141
|
+
`but was given handle '${agg.id}'.`,
|
|
142
|
+
);
|
|
143
|
+
}
|
|
144
|
+
const parsed = directive.payloadSchema.parse(payload);
|
|
145
|
+
const hlc = ctx.clock();
|
|
146
|
+
const marker = encodeRefMode(directive.marker);
|
|
147
|
+
// THE NOMOS SHAPE: a plan emits events across aggregates via TWO library doors, merged here —
|
|
148
|
+
// (1) the fluent authoring surface (`create`/`.set`/`.add`/`.relate`) records ops into the sink;
|
|
149
|
+
// (2) the plan may ALSO return explicit `PlannedOp`s (the typed `set`/`addToSet`/`strike` surface).
|
|
150
|
+
// Reset the sinks, run the plan, then merge authored ops (births/relations) ahead of the returned ops.
|
|
151
|
+
// The plan may also READ (O(1) DSL queries via `read(...)`) — captured into the footprint below.
|
|
152
|
+
__resetAuthoring();
|
|
153
|
+
__resetReads();
|
|
154
|
+
const returned = directive.plan(parsed, ctx);
|
|
155
|
+
const planned = [...__drainAuthoring(), ...returned];
|
|
156
|
+
const reads = __drainReads();
|
|
157
|
+
|
|
158
|
+
// Partition the plan output: STRIKE ops route onto the intent's `strikes` channel
|
|
159
|
+
// (the kernel's strikeout / revert facet, folded by parity); FIELD ops group into
|
|
160
|
+
// events below. A single plan may emit BOTH (strike X + author replacement ops =
|
|
161
|
+
// one atomic `replace` intent). Strikes are de-duped, authored order preserved.
|
|
162
|
+
const strikes: string[] = [];
|
|
163
|
+
const ops: FieldOp[] = [];
|
|
164
|
+
for (const op of planned) {
|
|
165
|
+
if (op.kind === "strike") {
|
|
166
|
+
if (!strikes.includes(op.target)) strikes.push(op.target);
|
|
167
|
+
} else {
|
|
168
|
+
ops.push(op);
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Group ops by aggregate id, preserving first-seen order (authored order).
|
|
173
|
+
// `aggregateId` is the address: for an unbound op it is the TYPE (legacy
|
|
174
|
+
// one-per-workspace shape); for an instance-bound op (`instance(h, id)`) it is
|
|
175
|
+
// the concrete INSTANCE id, with `aggregateType` carrying the TYPE (#105).
|
|
176
|
+
const order: string[] = [];
|
|
177
|
+
const byAggregate = new Map<string, WireFieldOp[]>();
|
|
178
|
+
// Per-bucket aggregate TYPE — only present for instance-bound ops. When set, we
|
|
179
|
+
// auto-stamp `__type`/`__id` provenance field-ops so `view()` (api.rs
|
|
180
|
+
// `project_view`) can recover the aggregate's TYPE off a per-instance id.
|
|
181
|
+
const typeOf = new Map<string, string>();
|
|
182
|
+
// Whether a bucket is the directive's OWN TARGET aggregate (the `creates`/`mutates`/…
|
|
183
|
+
// subject) vs an emitted sibling event. The directive's referential marker applies ONLY
|
|
184
|
+
// to its own target; an event emitted to ANOTHER aggregate is a MUTATE of an existing
|
|
185
|
+
// aggregate (NEVER a Create — the sibling is not being created here). This is the
|
|
186
|
+
// per-aggregate marker invariant the kernel id-mint gate now enforces: with the gate
|
|
187
|
+
// verifying EVERY `Create`, a `creates` directive that also touches a sibling aggregate
|
|
188
|
+
// (e.g. `createBuilding` adds the building to its parent site's `buildingIds`) must NOT mark the parent-site
|
|
189
|
+
// event `Create` — only the building. (Previously masked by the `__mintSeed` opt-in
|
|
190
|
+
// gate; the stronger gate makes the correct per-aggregate marker mandatory.)
|
|
191
|
+
const ownTarget = new Map<string, boolean>();
|
|
192
|
+
const explicitMarker = new Map<string, WireRefMode>();
|
|
193
|
+
const fieldsOf = agg.fields as Record<string, Field>;
|
|
194
|
+
for (const op of ops) {
|
|
195
|
+
let bucket = byAggregate.get(op.aggregateId);
|
|
196
|
+
if (bucket === undefined) {
|
|
197
|
+
bucket = [];
|
|
198
|
+
byAggregate.set(op.aggregateId, bucket);
|
|
199
|
+
order.push(op.aggregateId);
|
|
200
|
+
}
|
|
201
|
+
if (op.aggregateType !== undefined) typeOf.set(op.aggregateId, op.aggregateType);
|
|
202
|
+
if (op.marker !== undefined) {
|
|
203
|
+
const encoded = encodeRefMode(op.marker);
|
|
204
|
+
const existing = explicitMarker.get(op.aggregateId);
|
|
205
|
+
if (existing !== undefined && existing !== encoded) {
|
|
206
|
+
throw new Error(
|
|
207
|
+
`Directive '${directive.id}' emits conflicting markers for aggregate ` +
|
|
208
|
+
`'${op.aggregateId}': ${existing} vs ${encoded}.`,
|
|
209
|
+
);
|
|
210
|
+
}
|
|
211
|
+
explicitMarker.set(op.aggregateId, encoded);
|
|
212
|
+
}
|
|
213
|
+
// Field kinds are known for ops targeting the directive's own aggregate — by
|
|
214
|
+
// TYPE for a bound op (`aggregateType`), else by id for an unbound op. Ops
|
|
215
|
+
// fanning out to siblings encode by value-kind (the `json` path needs the kind).
|
|
216
|
+
const isOwnTarget =
|
|
217
|
+
op.aggregateType !== undefined ? op.aggregateType === agg.id : op.aggregateId === agg.id;
|
|
218
|
+
// A bucket is the own target if ANY of its ops target the directive's own aggregate.
|
|
219
|
+
if (isOwnTarget) ownTarget.set(op.aggregateId, true);
|
|
220
|
+
else if (!ownTarget.has(op.aggregateId)) ownTarget.set(op.aggregateId, false);
|
|
221
|
+
const field = isOwnTarget ? fieldsOf[op.field] : undefined;
|
|
222
|
+
bucket.push(encodeKernelFieldOp(op, field));
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
const events: WireEvent[] = order.map((aggregate) => {
|
|
226
|
+
const ops = byAggregate.get(aggregate)!;
|
|
227
|
+
const ty = typeOf.get(aggregate);
|
|
228
|
+
// The directive's marker applies to its OWN target; a FAN-OUT bucket is a Mutate of an
|
|
229
|
+
// existing sibling aggregate (so the id-mint gate never sees a spurious Create for it).
|
|
230
|
+
const eventMarker: WireRefMode =
|
|
231
|
+
explicitMarker.get(aggregate) ?? (ownTarget.get(aggregate) ? marker : "Mutate");
|
|
232
|
+
if (ty !== undefined) {
|
|
233
|
+
// Stamp provenance FIRST so the fold carries the TYPE + canonical id the
|
|
234
|
+
// structured `view()` projects (the same `__type`/`__id` reserved fields the
|
|
235
|
+
// Rust generic encoder path uses). Lww scalars — re-authoring is idempotent.
|
|
236
|
+
return {
|
|
237
|
+
aggregate,
|
|
238
|
+
marker: eventMarker,
|
|
239
|
+
ops: [
|
|
240
|
+
{ field: "__type", op: { Set: { Str: ty } } },
|
|
241
|
+
{ field: "__id", op: { Set: { Str: aggregate } } },
|
|
242
|
+
...ops,
|
|
243
|
+
],
|
|
244
|
+
};
|
|
245
|
+
}
|
|
246
|
+
return { aggregate, marker: eventMarker, ops };
|
|
247
|
+
});
|
|
248
|
+
// Attach the captured read footprint — OMITTED when empty so a read-free intent is byte-identical.
|
|
249
|
+
return { id: intentIdFromHlc(hlc), hlc, strikes, events, ...(reads.length ? { reads } : {}) };
|
|
250
|
+
}
|