@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
|
@@ -0,0 +1,144 @@
|
|
|
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 **relation registry** + **traversal certifier** for the entity-graph query
|
|
10
|
+
* vertical.
|
|
11
|
+
*
|
|
12
|
+
* ## A relation is already a `t.ref` field — no new field kind
|
|
13
|
+
* The ground truth (verified in `fields.ts:95`): a relation between two aggregates
|
|
14
|
+
* is ALREADY modelled as a ref field —
|
|
15
|
+
* `parentId: t.ref(Parent)` produces `Field{ kind:'ref', refAggregateId: 'ParentAggregate' }`.
|
|
16
|
+
* So the relation graph is NOT a new authoring construct; it is DERIVED by reading
|
|
17
|
+
* the `kind==='ref'` fields off the registered `AggregateHandle`s.
|
|
18
|
+
*
|
|
19
|
+
* `buildRelationRegistry(handles)` builds `Map<aggId, Map<field, targetAggId>>` — for
|
|
20
|
+
* each handle, every ref field becomes an edge `(aggId, field) → refAggregateId`.
|
|
21
|
+
*
|
|
22
|
+
* ## The traversal certifier (schema-evolution safety)
|
|
23
|
+
* `certifyTraversal` walks an authored hop list against the registry and FAILS at
|
|
24
|
+
* cert time (not run time) on:
|
|
25
|
+
* * a hop whose field does not exist on the current aggregate (a RENAMED ref →
|
|
26
|
+
* the edge is gone → fail; this is the schema-evolution guard),
|
|
27
|
+
* * a hop whose field is not a ref (you can only traverse along relations),
|
|
28
|
+
* * a hop whose target is not a REGISTERED aggregate,
|
|
29
|
+
* * a CROSS-WORKSPACE ref (`refWorkspace` set) — those are PR-tier edges the
|
|
30
|
+
* kernel routes, NOT same-DB joinable; reject them here,
|
|
31
|
+
* * exceeding the bounded traversal DEPTH (`MAX_DEPTH`),
|
|
32
|
+
* * revisiting an aggregate already on the path (CYCLE guard — e.g.
|
|
33
|
+
* `Child.parentId ↔ Parent.primaryChildId`).
|
|
34
|
+
*
|
|
35
|
+
* The certifier returns the resolved chain of `(fromAgg, field, toAgg)` hops so the
|
|
36
|
+
* compiler can lower each to a JOIN without re-deriving anything.
|
|
37
|
+
*/
|
|
38
|
+
import type { AggregateHandle } from "../aggregate.js";
|
|
39
|
+
import type { Field } from "../fields.js";
|
|
40
|
+
|
|
41
|
+
/** A registered ref edge: traversing `field` from `from` lands on `to`. */
|
|
42
|
+
export interface RefEdge {
|
|
43
|
+
readonly from: string;
|
|
44
|
+
readonly field: string;
|
|
45
|
+
readonly to: string;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** `Map<aggId, Map<field, targetAggId>>` — the derived same-workspace relation graph. */
|
|
49
|
+
export type RelationRegistry = Map<string, Map<string, string>>;
|
|
50
|
+
|
|
51
|
+
/** The set of every registered aggregate id (target-existence check). */
|
|
52
|
+
export interface CertifyContext {
|
|
53
|
+
readonly registry: RelationRegistry;
|
|
54
|
+
readonly known: ReadonlySet<string>;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Bounded traversal depth — the runtime backstop has a step budget; THIS is the
|
|
58
|
+
* cert-time bound so an unbounded/cyclic traversal never even compiles. */
|
|
59
|
+
export const MAX_DEPTH = 8;
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Derive the relation registry from the registered aggregate handles. Reads ONLY
|
|
63
|
+
* the `kind==='ref'` fields (with a same-workspace `refAggregateId`); cross-ws refs
|
|
64
|
+
* (`refWorkspace` set) are intentionally EXCLUDED from the joinable graph.
|
|
65
|
+
*/
|
|
66
|
+
export function buildRelationRegistry(
|
|
67
|
+
handles: readonly AggregateHandle[],
|
|
68
|
+
): CertifyContext {
|
|
69
|
+
const registry: RelationRegistry = new Map();
|
|
70
|
+
const known = new Set<string>();
|
|
71
|
+
for (const h of handles) known.add(h.id);
|
|
72
|
+
for (const h of handles) {
|
|
73
|
+
const edges = new Map<string, string>();
|
|
74
|
+
for (const [field, f] of Object.entries(h.fields as Record<string, Field>)) {
|
|
75
|
+
if (f.kind !== "ref") continue;
|
|
76
|
+
// Cross-workspace refs are NOT same-DB joinable — leave them out so a
|
|
77
|
+
// traversal over one fails the "field is not a (joinable) ref" check.
|
|
78
|
+
if (f.refWorkspace !== undefined) continue;
|
|
79
|
+
if (f.refAggregateId === undefined) continue;
|
|
80
|
+
edges.set(field, f.refAggregateId);
|
|
81
|
+
}
|
|
82
|
+
registry.set(h.id, edges);
|
|
83
|
+
}
|
|
84
|
+
return { registry, known };
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/** A traversal-certification failure carrying a human diagnostic. */
|
|
88
|
+
export class TraversalCertError extends Error {
|
|
89
|
+
constructor(message: string) {
|
|
90
|
+
super(message);
|
|
91
|
+
this.name = "TraversalCertError";
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Certify an authored hop list (`['siteId', ...]`) starting at `root`. Returns the
|
|
97
|
+
* resolved `RefEdge[]`. THROWS `TraversalCertError` on any of the guard violations.
|
|
98
|
+
*/
|
|
99
|
+
export function certifyTraversal(
|
|
100
|
+
ctx: CertifyContext,
|
|
101
|
+
root: string,
|
|
102
|
+
hops: readonly string[],
|
|
103
|
+
): RefEdge[] {
|
|
104
|
+
if (!ctx.known.has(root)) {
|
|
105
|
+
throw new TraversalCertError(`root aggregate '${root}' is not registered`);
|
|
106
|
+
}
|
|
107
|
+
if (hops.length > MAX_DEPTH) {
|
|
108
|
+
throw new TraversalCertError(
|
|
109
|
+
`traversal depth ${hops.length} exceeds MAX_DEPTH ${MAX_DEPTH}`,
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
const visited = new Set<string>([root]);
|
|
113
|
+
const out: RefEdge[] = [];
|
|
114
|
+
let current = root;
|
|
115
|
+
for (const field of hops) {
|
|
116
|
+
const edges = ctx.registry.get(current);
|
|
117
|
+
// current is always a known aggregate (root checked; each `to` checked below).
|
|
118
|
+
const target = edges?.get(field);
|
|
119
|
+
if (target === undefined) {
|
|
120
|
+
// Either the field does not exist, or it exists but is not a (same-ws) ref —
|
|
121
|
+
// both collapse to "not a traversable relation here". This is the
|
|
122
|
+
// schema-evolution guard: a renamed/retyped ref makes the edge vanish → FAIL.
|
|
123
|
+
throw new TraversalCertError(
|
|
124
|
+
`'${current}' has no traversable ref field '${field}' ` +
|
|
125
|
+
`(renamed/retyped/cross-workspace refs fail here)`,
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
if (!ctx.known.has(target)) {
|
|
129
|
+
throw new TraversalCertError(
|
|
130
|
+
`ref '${current}.${field}' targets unregistered aggregate '${target}'`,
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
if (visited.has(target)) {
|
|
134
|
+
throw new TraversalCertError(
|
|
135
|
+
`traversal cycle: '${target}' already visited on the path ` +
|
|
136
|
+
`(e.g. Child.parentId ↔ Parent.primaryChildId)`,
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
visited.add(target);
|
|
140
|
+
out.push({ from: current, field, to: target });
|
|
141
|
+
current = target;
|
|
142
|
+
}
|
|
143
|
+
return out;
|
|
144
|
+
}
|
package/src/query.ts
ADDED
|
@@ -0,0 +1,151 @@
|
|
|
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
|
+
* `query(id)` builder — the DECLARATION layer of Nomos's read-side closure.
|
|
10
|
+
*
|
|
11
|
+
* READ-CLOSURE step 1. Measured: the whole-workspace `view()` fold is 24.3s at
|
|
12
|
+
* 100k intents; an indexed read of the same is 278µs. The fix is structural, not a
|
|
13
|
+
* perf tweak: a domain DECLARES its reads as NAMED, INDEXED queries; the compiler
|
|
14
|
+
* (a LATER step — the Rust read engine / FRB / `view()` deletion) generates the
|
|
15
|
+
* index + a typesafe query system from these declarations; and — because every
|
|
16
|
+
* query MUST carry an index key — an un-indexed read becomes INEXPRESSIBLE.
|
|
17
|
+
*
|
|
18
|
+
* This module adds ONLY the declaration + its TYPE-STATE. It mirrors the
|
|
19
|
+
* additive, omit-when-empty, identity-bearing pattern of `directive.ts`'s
|
|
20
|
+
* `.reads()`/`.emits()`: a query a domain declares is carried into the canonical
|
|
21
|
+
* manifest (and USD IR) and becomes part of the domain IDENTITY; a domain that
|
|
22
|
+
* declares NO query is byte-identical to before this existed.
|
|
23
|
+
*
|
|
24
|
+
* The TYPE-STATE is the mechanical boundary's first brick. `query(id)` yields a
|
|
25
|
+
* `KeylessQuery` whose ONLY method is `.key(...)`; `.returns(...)`/registration
|
|
26
|
+
* live solely on the `Query` that `.key(...)` produces. So a query with no index
|
|
27
|
+
* key cannot even be CONSTRUCTED — "no un-indexed query is expressible" is proven
|
|
28
|
+
* at the type level, before any runtime check.
|
|
29
|
+
*/
|
|
30
|
+
import type { AggregateHandle } from "./aggregate.js";
|
|
31
|
+
import type { Field } from "./fields.js";
|
|
32
|
+
|
|
33
|
+
type StringKeyOf<T> = Extract<keyof T, string>;
|
|
34
|
+
|
|
35
|
+
type JsonFieldKeys<F extends Record<string, Field>> = {
|
|
36
|
+
[K in StringKeyOf<F>]: F[K] extends Field<unknown, "json"> ? K : never;
|
|
37
|
+
}[StringKeyOf<F>];
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Legal indexed key paths for a returned aggregate:
|
|
41
|
+
* - a declared top-level aggregate field; or
|
|
42
|
+
* - a dotted path rooted at a JSON leaf (`placement.siteId`), which the read
|
|
43
|
+
* manifest routes as an explicit nested index.
|
|
44
|
+
*/
|
|
45
|
+
export type QueryableFieldPath<F extends Record<string, Field>> =
|
|
46
|
+
| StringKeyOf<F>
|
|
47
|
+
| `${JsonFieldKeys<F>}.${string}`;
|
|
48
|
+
|
|
49
|
+
type QueryKeyCompatibility<
|
|
50
|
+
K extends readonly string[],
|
|
51
|
+
F extends Record<string, Field>,
|
|
52
|
+
> = Exclude<K[number], QueryableFieldPath<F>> extends never
|
|
53
|
+
? unknown
|
|
54
|
+
: {
|
|
55
|
+
readonly __query_key_error__: `query key '${Exclude<K[number], QueryableFieldPath<F>>}' is not a field on returned aggregate`;
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* A FINISHED query declaration: an id, the INDEX KEY (the ordered field list the
|
|
60
|
+
* generated index is built over — order is the index column order, PRESERVED, not
|
|
61
|
+
* sorted), and the aggregate TYPE id it `returns`. Minimal by design: params /
|
|
62
|
+
* where / scope are later steps.
|
|
63
|
+
*/
|
|
64
|
+
export interface QueryDecl {
|
|
65
|
+
readonly id: string;
|
|
66
|
+
/** Index key fields, in DECLARED order (= index column order). */
|
|
67
|
+
readonly key: string[];
|
|
68
|
+
/** The aggregate TYPE id the query returns, e.g. `SampleThingAggregate`. */
|
|
69
|
+
readonly returns: string;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* The managed read indexes IMPLIED by an aggregate's `t.hasMany` relations. Each
|
|
74
|
+
* `t.hasMany(Child).via("parent")` field declares the inverse index `Child by parent` — the read side
|
|
75
|
+
* the framework maintains so `parent.children` is an O(result) lookup, not a scan. The dev writes no
|
|
76
|
+
* `query(...)`: the relation IS the index. Deterministic (sorted by id), so it can be merged into the
|
|
77
|
+
* aggregate's declared queries without changing order-dependent output.
|
|
78
|
+
*/
|
|
79
|
+
export function hasManyIndexes(agg: AggregateHandle): QueryDecl[] {
|
|
80
|
+
const out: QueryDecl[] = [];
|
|
81
|
+
// `agg.hasMany` holds exactly the virtual inverses, so no kind check is needed here —
|
|
82
|
+
// the partition in `aggregate()` already guarantees every member is a `t.hasMany`.
|
|
83
|
+
for (const field of Object.values(agg.hasMany as Record<string, Field>)) {
|
|
84
|
+
if (field.refAggregateId !== undefined && field.viaField !== undefined) {
|
|
85
|
+
out.push({
|
|
86
|
+
id: `${field.refAggregateId}_by_${field.viaField}`,
|
|
87
|
+
key: [field.viaField],
|
|
88
|
+
returns: field.refAggregateId,
|
|
89
|
+
});
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
return out.sort((a, b) => a.id.localeCompare(b.id));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* The registerable form of a query: it has an index key, so `.returns(...)` (which
|
|
97
|
+
* fixes the returned aggregate type and yields the finished {@link QueryDecl}) is
|
|
98
|
+
* available. This is the ONLY shape carrying `.returns` — see {@link KeylessQuery}.
|
|
99
|
+
*/
|
|
100
|
+
export interface Query<K extends readonly string[] = readonly string[]> {
|
|
101
|
+
readonly id: string;
|
|
102
|
+
readonly key: readonly [...K];
|
|
103
|
+
/**
|
|
104
|
+
* Fix the aggregate TYPE this query returns, producing the finished declaration.
|
|
105
|
+
* Takes a typed HANDLE (never the string id) — a typo'd handle is a compile
|
|
106
|
+
* error, the same convention as `directive`'s referential markers.
|
|
107
|
+
*/
|
|
108
|
+
returns<const Id extends string, F extends Record<string, Field>>(
|
|
109
|
+
aggregate: AggregateHandle<Id, F> & QueryKeyCompatibility<K, F>,
|
|
110
|
+
): QueryDecl;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* The INITIAL, un-keyed query — its ONLY method is `.key(...)`. It deliberately has
|
|
115
|
+
* NO `.returns` and is NOT a `QueryDecl`, so `query("q").returns(...)` (skipping the
|
|
116
|
+
* index key) is a COMPILE error and a key-less query cannot be constructed. THIS is
|
|
117
|
+
* the type-level "no un-indexed query expressible" property.
|
|
118
|
+
*/
|
|
119
|
+
export interface KeylessQuery {
|
|
120
|
+
readonly id: string;
|
|
121
|
+
/**
|
|
122
|
+
* Declare the INDEX KEY — the ordered field list the generated index is built
|
|
123
|
+
* over. Order is significant (index column order) and is PRESERVED, never sorted.
|
|
124
|
+
* Returns the registerable {@link Query} (the only shape exposing `.returns`).
|
|
125
|
+
*/
|
|
126
|
+
key<const K extends readonly [string, ...string[]]>(...fields: K): Query<K>;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Begin a query declaration. Returns a {@link KeylessQuery}: until `.key(...)` is
|
|
131
|
+
* called, neither `.returns` nor a finished `QueryDecl` exists — the index key is
|
|
132
|
+
* NOT optional, it is a prerequisite for the query to take any further shape.
|
|
133
|
+
*/
|
|
134
|
+
export function query<const Id extends string>(id: Id): KeylessQuery {
|
|
135
|
+
return {
|
|
136
|
+
id,
|
|
137
|
+
key<const K extends readonly [string, ...string[]]>(...fields: K): Query<K> {
|
|
138
|
+
// Preserve declared order — this list is the index's column order, not a set.
|
|
139
|
+
const keyFields = [...fields] as unknown as readonly [...K];
|
|
140
|
+
return {
|
|
141
|
+
id,
|
|
142
|
+
key: keyFields,
|
|
143
|
+
returns<const AggId extends string, F extends Record<string, Field>>(
|
|
144
|
+
aggregate: AggregateHandle<AggId, F> & QueryKeyCompatibility<K, F>,
|
|
145
|
+
): QueryDecl {
|
|
146
|
+
return { id, key: [...keyFields], returns: aggregate.id };
|
|
147
|
+
},
|
|
148
|
+
};
|
|
149
|
+
},
|
|
150
|
+
};
|
|
151
|
+
}
|
package/src/read.ts
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
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}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* THE CAPTURED READ — the plan's read side, made deterministic (Jack 2026-06-08).
|
|
8
|
+
*
|
|
9
|
+
* A plan that can't read can't decide anything; so it reads — through DECLARED, O(1) DSL queries (a
|
|
10
|
+
* managed index, never a scan). The catch that keeps determinism: every read is CAPTURED. The library
|
|
11
|
+
* calls the ONE host read capability `nomos.read(queryId, args)`, records `{queryId, args, result}` into
|
|
12
|
+
* a per-dispatch footprint, and `executeDirectiveToIntent` attaches it to the intent (all taken at the intent's HLC).
|
|
13
|
+
*
|
|
14
|
+
* `nomos.read` is the exact twin of `nomos.rng`: a LIVE source at author (the host serves it from the
|
|
15
|
+
* projection at the current tip), a CAPTURE replayed at verify (the host returns the recorded result —
|
|
16
|
+
* never a live re-query). So a write commits both halves of its truth: the events it wrote AND the queries
|
|
17
|
+
* it read to justify them. The dev never sees `nomos.read` — they call typed DSL queries; the library
|
|
18
|
+
* routes them here. (See `docs/aggregate_lifecycle_and_relations.md`.)
|
|
19
|
+
*/
|
|
20
|
+
import type { QueryDecl } from "./query.js";
|
|
21
|
+
import type { WireRead } from "./wire.js";
|
|
22
|
+
|
|
23
|
+
// ── the per-dispatch read footprint — library state drained by `executeDirectiveToIntent` (mirrors the authoring sink) ──
|
|
24
|
+
let footprint: WireRead[] = [];
|
|
25
|
+
/** Clear the footprint before a plan runs (called by `executeDirectiveToIntent`). */
|
|
26
|
+
export function __resetReads(): void {
|
|
27
|
+
footprint = [];
|
|
28
|
+
}
|
|
29
|
+
/** Take + clear the reads captured during a plan (called by `executeDirectiveToIntent`). */
|
|
30
|
+
export function __drainReads(): WireRead[] {
|
|
31
|
+
const f = footprint;
|
|
32
|
+
footprint = [];
|
|
33
|
+
return f;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
interface NomosRead {
|
|
37
|
+
read(queryId: string, args: Record<string, string>): unknown;
|
|
38
|
+
}
|
|
39
|
+
/** The ONE host read capability — an O(1) projection read (live at author, captured-replay at verify). */
|
|
40
|
+
function host(): NomosRead {
|
|
41
|
+
return (globalThis as unknown as { nomos: NomosRead }).nomos;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Read an O(1) DSL-defined query on the write path. Returns the result AND captures `{queryId, args,
|
|
46
|
+
* result}` into the intent's read footprint — so the read is replayable and the write's premise is
|
|
47
|
+
* committed. `query` is a declared, indexed {@link QueryDecl} (e.g. a `t.hasMany` inverse from
|
|
48
|
+
* `hasManyIndexes`); `args` are its index-key values.
|
|
49
|
+
*/
|
|
50
|
+
export function read<T = unknown>(query: QueryDecl, args: Record<string, string>): T {
|
|
51
|
+
const result = host().read(query.id, args) as T;
|
|
52
|
+
footprint.push({ queryId: query.id, args, result });
|
|
53
|
+
return result;
|
|
54
|
+
}
|
package/src/relation.ts
ADDED
|
@@ -0,0 +1,189 @@
|
|
|
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
|
+
// The cross-workspace `relation` declaration (`cross_workspace.md` §2). A TARGET domain
|
|
9
|
+
// declares, on the directive a cross-workspace PR proposes, the relation it will adjudicate:
|
|
10
|
+
// the source/target aggregate endpoints, the BOUNDED evidence read it discloses (the
|
|
11
|
+
// disclosure contract + anti-smuggle key, §2.4), and that an invariant predicate guards it.
|
|
12
|
+
//
|
|
13
|
+
// This is DECLARATION ONLY — symmetric with `emits`/`requires`/`readsCertified`. It lowers
|
|
14
|
+
// into the domain manifest IR (`manifest.ts`, the hash-pinned #136 identity), which the ONE
|
|
15
|
+
// write-path gate (`executor::admit` step 5.5 / `kernel::manifest_view`) resolves to adjudicate
|
|
16
|
+
// a carried PR against the target's OWN law. The invariant PREDICATE itself is executable
|
|
17
|
+
// behaviour (a guard-shaped pure fn over `(target state, carried evidence)`, `evidence.md`
|
|
18
|
+
// §4b) — supplied to the gate as the domain's runtime guard, NOT captured in the manifest
|
|
19
|
+
// (only its PRESENCE is hashed, exactly like a directive `plan`).
|
|
20
|
+
|
|
21
|
+
/** Freshness of the disclosed source fact (`cross_workspace.md` §2.4). `immutable`
|
|
22
|
+
* (set-once, e.g. `createdBy`) — any commit proves it. `current-state` (e.g. "is currently an
|
|
23
|
+
* owner") — point-in-time at the witnessed commit; the target combines it with its own
|
|
24
|
+
* current state per §3.5. */
|
|
25
|
+
export type EvidenceKind = "immutable" | "current-state";
|
|
26
|
+
|
|
27
|
+
/** The BOUNDED evidence read a relation discloses over the SOURCE (`cross_workspace.md` §2.4):
|
|
28
|
+
* exactly the fields the invariant reads, the predicate over the source aggregate, and the
|
|
29
|
+
* freshness kind. This IS the disclosure contract (the target learns these fields and NOTHING
|
|
30
|
+
* else) AND the anti-smuggle key (a carried witness must match this declared read exactly). */
|
|
31
|
+
export interface RelationEvidence {
|
|
32
|
+
/** The source aggregate TYPE the read selects from (e.g. `ThingAggregate`). */
|
|
33
|
+
readonly aggregate: string;
|
|
34
|
+
/** The exact fields disclosed (the SELECT list). The SET is the contract. */
|
|
35
|
+
readonly select: string[];
|
|
36
|
+
/** The bounded predicate over the source (the WHERE) — the read's scope. */
|
|
37
|
+
readonly where: string;
|
|
38
|
+
/** `immutable` or `current-state` — governs whether freshness bites (§3.5). */
|
|
39
|
+
readonly kind: EvidenceKind;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The PROVEN facts a relation invariant predicate consumes (`cross_workspace.md` §2.4): the
|
|
43
|
+
* verified carried-evidence fields (the declared `select`, e.g. `createdBy`/`thingId`), the
|
|
44
|
+
* PROVEN author of the source commit, and the proposed target directive's own payload arguments.
|
|
45
|
+
* This is deliberately small: no relation recipe, no hidden target-state summary, no generated
|
|
46
|
+
* write authority. If a domain needs more target-owned facts, it must declare/read them through
|
|
47
|
+
* ordinary Nomos law, not through a manifest-side cross-workspace template. */
|
|
48
|
+
export interface InvariantEvidence {
|
|
49
|
+
/** The disclosed source fields → values (EXACTLY the relation's declared `select`). The
|
|
50
|
+
* least-disclosure surface, e.g. `{ createdBy: "david", thingId: "thing-A" }`. The body reads
|
|
51
|
+
* the tenant-specific field names IT declared in the relation's `select` here. */
|
|
52
|
+
readonly fields: Record<string, string>;
|
|
53
|
+
/** The PROVEN author of the source commit (the verified `envelope.json` attribution actor). */
|
|
54
|
+
readonly authoredBy: string;
|
|
55
|
+
/** The target directive payload as proposed by the incoming ordinary intent. The invariant
|
|
56
|
+
* compares these arguments with `fields` and `authoredBy`; it does not receive authority to
|
|
57
|
+
* synthesize any other write. */
|
|
58
|
+
readonly proposed: Record<string, string>;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The verdict an invariant body returns (`cross_workspace.md` §2.1 / §4b — symmetric with the
|
|
62
|
+
* `guard` outcome union). `{ accept: true }` ⇒ the PR's `proposes` merges; `{ reject: code }` ⇒
|
|
63
|
+
* a typed decline (the fail receipt carries ONLY `code`, §2.4 least-disclosure). */
|
|
64
|
+
export type InvariantVerdict = { readonly accept: true } | { readonly reject: string };
|
|
65
|
+
|
|
66
|
+
/** The EXECUTABLE invariant predicate body (`cross_workspace.md` §2.1 / §4b): a guard-shaped PURE
|
|
67
|
+
* function of `(the TARGET's own current state, the PROVEN carried evidence)` → {@link
|
|
68
|
+
* InvariantVerdict}. It reads ONLY the §1.1 enforceable set (carried self-verifying evidence +
|
|
69
|
+
* target authoritative state, both supplied in {@link InvariantEvidence}) — NEVER a live source
|
|
70
|
+
* read ([[determinism-or-death]]). The body ships in the engine bundle (compiled, like a directive
|
|
71
|
+
* `plan`), NOT in the manifest (only its PRESENCE is hashed); the gate evaluates it IN THE SEALED
|
|
72
|
+
* ENGINE over the carried evidence + the target state — the SAME engine path `plan()` uses. */
|
|
73
|
+
export type InvariantBody = (evidence: InvariantEvidence) => InvariantVerdict;
|
|
74
|
+
|
|
75
|
+
/** A finished cross-workspace relation declaration (`cross_workspace.md` §2.1). Attached to a
|
|
76
|
+
* directive via `.declaresRelation(...)`; lowered into the manifest IR keyed by `proposes`. */
|
|
77
|
+
export interface RelationDecl {
|
|
78
|
+
/** The relation id (e.g. `ThingOwnership`). */
|
|
79
|
+
readonly id: string;
|
|
80
|
+
/** The SOURCE aggregate TYPE that PROPOSES the contribution (e.g. `ThingAggregate`). */
|
|
81
|
+
readonly source: string;
|
|
82
|
+
/** The TARGET aggregate TYPE that ACCEPTS the contribution (e.g. `UserPermission`). */
|
|
83
|
+
readonly target: string;
|
|
84
|
+
/** The TARGET directive the relation proposes into (the gate's lookup key; e.g. `grantOwner`).
|
|
85
|
+
* Defaults to the directive the relation is declared on. */
|
|
86
|
+
readonly proposes: string;
|
|
87
|
+
/** The bounded evidence read over the source — the disclosure contract. */
|
|
88
|
+
readonly evidence: RelationEvidence;
|
|
89
|
+
/** Whether an invariant predicate is declared (presence only — the body is executable, not
|
|
90
|
+
* hashed; the gate evaluates the domain's runtime guard). */
|
|
91
|
+
readonly hasInvariant: boolean;
|
|
92
|
+
/** The EXECUTABLE invariant predicate body (`cross_workspace.md` §2.1). `undefined` when no
|
|
93
|
+
* invariant is declared (`hasInvariant: false`). The body is COMPILED INTO THE ENGINE BUNDLE
|
|
94
|
+
* (not the manifest — only `hasInvariant` is hashed, exactly like a directive `plan`); the gate
|
|
95
|
+
* evaluates it in the sealed engine over the carried evidence + target state (the determinism /
|
|
96
|
+
* sovereignty core — the verdict is read from THIS domain law, not transcribed into Rust). */
|
|
97
|
+
readonly invariant?: InvariantBody;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/** Stage 1 of the builder: id + the two endpoints. */
|
|
101
|
+
class RelationSourceStep<Id extends string> {
|
|
102
|
+
constructor(private readonly id: Id) {}
|
|
103
|
+
/** Declare the SOURCE → TARGET endpoints (aggregate TYPE ids). */
|
|
104
|
+
endpoints(source: string, target: string): RelationProposesStep<Id> {
|
|
105
|
+
return new RelationProposesStep(this.id, source, target);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/** Stage 2: the proposed target directive + the bounded evidence read. */
|
|
110
|
+
class RelationProposesStep<Id extends string> {
|
|
111
|
+
constructor(
|
|
112
|
+
private readonly id: Id,
|
|
113
|
+
private readonly source: string,
|
|
114
|
+
private readonly target: string,
|
|
115
|
+
) {}
|
|
116
|
+
/** Declare the target directive the relation proposes + the BOUNDED evidence read (the
|
|
117
|
+
* disclosure contract). `hasInvariant` defaults true (a relation without a guard would admit
|
|
118
|
+
* any signed evidence — the dev opts OUT explicitly via `{ hasInvariant: false }`, never by
|
|
119
|
+
* omission).
|
|
120
|
+
*
|
|
121
|
+
* The INVARIANT BODY (`opts.invariant`, or the chained `.invariant(fn)`) is the EXECUTABLE
|
|
122
|
+
* guard-shaped predicate the gate evaluates IN THE SEALED ENGINE over the carried evidence +
|
|
123
|
+
* the target's own state (`cross_workspace.md` §2.1). It is compiled into the engine bundle,
|
|
124
|
+
* NOT the manifest (only `hasInvariant` is hashed, like a directive `plan`) — so refactoring
|
|
125
|
+
* the predicate without changing its declared shape does not move the domain hash, yet the
|
|
126
|
+
* gate verdict is READ FROM THIS DOMAIN LAW (determinism/sovereignty core), never a Rust
|
|
127
|
+
* transcription. Returns a {@link RelationProposedStep} carrying the chainable `.invariant`. */
|
|
128
|
+
proposes(
|
|
129
|
+
directive: string,
|
|
130
|
+
evidence: RelationEvidence,
|
|
131
|
+
opts: { hasInvariant?: boolean; invariant?: InvariantBody } = {},
|
|
132
|
+
): RelationProposedStep {
|
|
133
|
+
return new RelationProposedStep({
|
|
134
|
+
id: this.id,
|
|
135
|
+
source: this.source,
|
|
136
|
+
target: this.target,
|
|
137
|
+
proposes: directive,
|
|
138
|
+
// Sort the disclosed fields — the SET is the contract (and the Rust view sorts too, so the
|
|
139
|
+
// manifest is byte-stable regardless of authoring order).
|
|
140
|
+
evidence: { ...evidence, select: [...evidence.select].sort() },
|
|
141
|
+
// An explicit body implies presence; otherwise `hasInvariant` defaults true. A relation that
|
|
142
|
+
// declares an invariant body but `{ hasInvariant: false }` is a contradiction the builder
|
|
143
|
+
// refuses to encode (presence MUST track the body).
|
|
144
|
+
hasInvariant: opts.invariant !== undefined ? true : (opts.hasInvariant ?? true),
|
|
145
|
+
...(opts.invariant !== undefined ? { invariant: opts.invariant } : {}),
|
|
146
|
+
});
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Stage 3: the finished relation, chainable with `.invariant(fn)` to attach the EXECUTABLE
|
|
151
|
+
* predicate body (`cross_workspace.md` §2.1). A {@link RelationProposedStep} IS a finished
|
|
152
|
+
* {@link RelationDecl} (the `.declaresRelation(...)` site accepts it directly), with the extra
|
|
153
|
+
* `.invariant(...)` ergonomic — `relation("X").endpoints(s,t).proposes(d, read).invariant(fn)`. */
|
|
154
|
+
class RelationProposedStep implements RelationDecl {
|
|
155
|
+
readonly id: string;
|
|
156
|
+
readonly source: string;
|
|
157
|
+
readonly target: string;
|
|
158
|
+
readonly proposes: string;
|
|
159
|
+
readonly evidence: RelationEvidence;
|
|
160
|
+
readonly hasInvariant: boolean;
|
|
161
|
+
readonly invariant?: InvariantBody;
|
|
162
|
+
constructor(decl: RelationDecl) {
|
|
163
|
+
this.id = decl.id;
|
|
164
|
+
this.source = decl.source;
|
|
165
|
+
this.target = decl.target;
|
|
166
|
+
this.proposes = decl.proposes;
|
|
167
|
+
this.evidence = decl.evidence;
|
|
168
|
+
this.hasInvariant = decl.hasInvariant;
|
|
169
|
+
// `exactOptionalPropertyTypes`: only set the optional `invariant` when present (never
|
|
170
|
+
// assign an explicit `undefined`), so a relation without executable body omits the property.
|
|
171
|
+
if (decl.invariant !== undefined) this.invariant = decl.invariant;
|
|
172
|
+
}
|
|
173
|
+
/** Attach the EXECUTABLE invariant predicate body — the guard-shaped pure fn the gate evaluates
|
|
174
|
+
* in the sealed engine over `(target state, carried evidence)` (`cross_workspace.md` §2.1 / §4b).
|
|
175
|
+
* Sets `hasInvariant: true` (presence tracks the body). Returns a NEW step (non-destructive).
|
|
176
|
+
* Reads `relation("X").endpoints(s,t).proposes(d, read).withInvariant((ev) => …)` at the site. */
|
|
177
|
+
withInvariant(fn: InvariantBody): RelationProposedStep {
|
|
178
|
+
return new RelationProposedStep({ ...(this as RelationDecl), hasInvariant: true, invariant: fn });
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** Begin a cross-workspace relation declaration:
|
|
183
|
+
* `relation("ThingOwnership").endpoints("ThingAggregate","UserPermission")
|
|
184
|
+
* .proposes("grantOwner", { aggregate:"ThingAggregate", select:["createdBy","thingId"],
|
|
185
|
+
* where:"thingId == $thingId", kind:"immutable" })`.
|
|
186
|
+
* Pure declaration — lowered into the manifest IR (`cross_workspace.md` §2). */
|
|
187
|
+
export function relation<const Id extends string>(id: Id): RelationSourceStep<Id> {
|
|
188
|
+
return new RelationSourceStep(id);
|
|
189
|
+
}
|
|
@@ -0,0 +1,54 @@
|
|
|
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
|
+
* `csv(rows, columns)` — a tiny deterministic CSV builder for report renders.
|
|
10
|
+
*
|
|
11
|
+
* Runs INSIDE the sealed engine over the host-fed `queryRows` (the render is a pure
|
|
12
|
+
* function — no clock, no rng, no ambient). Deterministic by construction: fixed
|
|
13
|
+
* column order, RFC-4180 quoting, `\n` line endings (never the platform's), and the
|
|
14
|
+
* rows arrive already totally-ordered from the host query port.
|
|
15
|
+
*/
|
|
16
|
+
import type { QueryRow, SqlRow } from "../report.js";
|
|
17
|
+
|
|
18
|
+
/** RFC-4180 field quoting: quote iff the field contains `,`, `"`, or a newline. */
|
|
19
|
+
function quote(field: string): string {
|
|
20
|
+
if (/[",\n\r]/.test(field)) {
|
|
21
|
+
return '"' + field.replace(/"/g, '""') + '"';
|
|
22
|
+
}
|
|
23
|
+
return field;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Render `rows` to CSV over the named `columns` (in order). Each column name is a
|
|
28
|
+
* key of `QueryRow`. The header row is the column names; each data row is the
|
|
29
|
+
* stringified field values. `\n` separated, trailing newline — a stable byte shape.
|
|
30
|
+
*/
|
|
31
|
+
export function csv(rows: QueryRow[], columns: (keyof QueryRow)[]): string {
|
|
32
|
+
const lines: string[] = [];
|
|
33
|
+
lines.push(columns.map((c) => quote(String(c))).join(","));
|
|
34
|
+
for (const row of rows) {
|
|
35
|
+
lines.push(columns.map((c) => quote(String(row[c]))).join(","));
|
|
36
|
+
}
|
|
37
|
+
return lines.join("\n") + "\n";
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Render `SqlRow`s (sealed-SQL report rows: column name → cell text) to CSV over
|
|
42
|
+
* the named `columns` in order. Same deterministic byte shape as `csv` — fixed
|
|
43
|
+
* column order, RFC-4180 quoting, `\n` endings — but keyed by string column names
|
|
44
|
+
* (the sealed query's SELECT aliases) rather than the fixed `QueryRow` shape. A
|
|
45
|
+
* missing column renders as the empty field (the SELECT alias governs presence).
|
|
46
|
+
*/
|
|
47
|
+
export function csvRows(rows: SqlRow[], columns: string[]): string {
|
|
48
|
+
const lines: string[] = [];
|
|
49
|
+
lines.push(columns.map((c) => quote(c)).join(","));
|
|
50
|
+
for (const row of rows) {
|
|
51
|
+
lines.push(columns.map((c) => quote(row[c] ?? "")).join(","));
|
|
52
|
+
}
|
|
53
|
+
return lines.join("\n") + "\n";
|
|
54
|
+
}
|