@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.
Files changed (53) hide show
  1. package/LICENSE.md +36 -0
  2. package/compile_package.mjs +50 -0
  3. package/package.json +59 -0
  4. package/src/aggregate.ts +167 -0
  5. package/src/authoring.ts +119 -0
  6. package/src/build_package.ts +636 -0
  7. package/src/certified_read.ts +313 -0
  8. package/src/codegen_dart.ts +2732 -0
  9. package/src/codegen_dot.ts +466 -0
  10. package/src/codegen_provider_dart.ts +358 -0
  11. package/src/codegen_ts.ts +365 -0
  12. package/src/codegen_usda.ts +388 -0
  13. package/src/combined.ts +195 -0
  14. package/src/compile_engine.ts +567 -0
  15. package/src/compile_package_main.ts +496 -0
  16. package/src/compose.ts +317 -0
  17. package/src/count.ts +218 -0
  18. package/src/ctx.ts +57 -0
  19. package/src/derived.ts +138 -0
  20. package/src/directive.ts +306 -0
  21. package/src/drivers.ts +95 -0
  22. package/src/emits_guard.ts +123 -0
  23. package/src/engine_entry.ts +449 -0
  24. package/src/exists.ts +170 -0
  25. package/src/extremum.ts +227 -0
  26. package/src/fields.ts +291 -0
  27. package/src/framework/bootstrap.ts +22 -0
  28. package/src/framework/disclosure.ts +108 -0
  29. package/src/framework/domain_lifecycle.ts +108 -0
  30. package/src/framework/identity.ts +537 -0
  31. package/src/framework/impure_capability.ts +643 -0
  32. package/src/framework/rbac.ts +418 -0
  33. package/src/framework/repair.ts +150 -0
  34. package/src/framework/sync_lifecycle.ts +125 -0
  35. package/src/framework/workspace_invariant.ts +128 -0
  36. package/src/framework/workspaces.ts +817 -0
  37. package/src/index.ts +317 -0
  38. package/src/manifest.ts +947 -0
  39. package/src/ops.ts +145 -0
  40. package/src/ordered_read.ts +228 -0
  41. package/src/predicate.ts +203 -0
  42. package/src/query/compile.ts +0 -0
  43. package/src/query/relations.ts +144 -0
  44. package/src/query.ts +151 -0
  45. package/src/read.ts +54 -0
  46. package/src/relation.ts +189 -0
  47. package/src/report/csv.ts +54 -0
  48. package/src/report.ts +401 -0
  49. package/src/spatial.ts +115 -0
  50. package/src/sum.ts +194 -0
  51. package/src/usd.ts +563 -0
  52. package/src/wire.ts +149 -0
  53. 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
+ }