@githolon/dsl 0.8.0 → 0.9.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/codegen_dart.ts +16 -0
- package/src/codegen_ts.ts +6 -0
- package/src/directive.ts +99 -10
- package/src/fields.ts +24 -2
- package/src/framework/home.ts +149 -0
- package/src/index.ts +16 -1
- package/src/ops.ts +50 -1
- package/src/usd.ts +62 -2
- package/src/usd_layers.ts +28 -0
- package/src/wire.ts +18 -3
- package/src/wire_encode.ts +28 -2
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.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_dart.ts
CHANGED
|
@@ -612,6 +612,13 @@ function aggFieldSpec(
|
|
|
612
612
|
}
|
|
613
613
|
case "int":
|
|
614
614
|
return { name, wireName, kind: field.kind, dartType: "int", optional };
|
|
615
|
+
// A PN-COUNTER reads as its net count and a POSITION as its order-key — the read model collapses
|
|
616
|
+
// them (kernel `observed_value`) to a scalar Int / Str, so codegen treats them AS `int` / `string`
|
|
617
|
+
// (the spec carries the NORMALIZED kind so every downstream reader path is unchanged).
|
|
618
|
+
case "counter":
|
|
619
|
+
return { name, wireName, kind: "int", dartType: "int", optional };
|
|
620
|
+
case "position":
|
|
621
|
+
return { name, wireName, kind: "string", dartType: valueClass ?? "String", optional, ...(valueClass !== undefined ? { valueClass } : {}) };
|
|
615
622
|
case "bool":
|
|
616
623
|
return { name, wireName, kind: field.kind, dartType: "bool", optional };
|
|
617
624
|
case "enum": {
|
|
@@ -1195,6 +1202,8 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
|
|
|
1195
1202
|
? `() { final v = ${reader}; return v == null ? null : ${s.refIdClass}.fromRow(v); }()`
|
|
1196
1203
|
: `${s.refIdClass}.fromRow(${reader})`;
|
|
1197
1204
|
}
|
|
1205
|
+
// `position` normalizes to `string`, `counter` to `int` in `aggFieldSpec` — unreachable here.
|
|
1206
|
+
case "position":
|
|
1198
1207
|
case "string":
|
|
1199
1208
|
if (s.valueClass !== undefined) {
|
|
1200
1209
|
const reader = opt
|
|
@@ -1217,6 +1226,7 @@ function parseField(s: AggFieldSpec, aggregate: string): string {
|
|
|
1217
1226
|
// The projection decodes a bool field to a real JSON boolean — read it directly (no runtime
|
|
1218
1227
|
// helper). Optional → nullable cast; required → a plain cast (absent ⇒ throws, like readInt).
|
|
1219
1228
|
return opt ? `(data['${s.wireName}'] as bool?)` : `(data['${s.wireName}'] as bool)`;
|
|
1229
|
+
case "counter":
|
|
1220
1230
|
case "int":
|
|
1221
1231
|
return opt
|
|
1222
1232
|
? `readIntOrNull(data, '${aggregate}', '${s.wireName}')`
|
|
@@ -1277,8 +1287,12 @@ function readModelDartType(s: AggFieldSpec): string {
|
|
|
1277
1287
|
// leaf — the kernel `Value` is Str|Int only), so it is surfaced as the permissive
|
|
1278
1288
|
// `Object?` (the read engine's decoded value as-is — never coerced/thrown).
|
|
1279
1289
|
return isJsonObjectLeaf(s) ? "Map<String, Object?>" : "Object?";
|
|
1290
|
+
// `position`/`counter` are normalized to `string`/`int` in `aggFieldSpec` (read-collapsed scalars),
|
|
1291
|
+
// so these labels are unreachable at runtime — present for FieldKind exhaustiveness.
|
|
1292
|
+
case "position":
|
|
1280
1293
|
case "string":
|
|
1281
1294
|
return s.valueClass ?? "String";
|
|
1295
|
+
case "counter":
|
|
1282
1296
|
case "int":
|
|
1283
1297
|
return "int";
|
|
1284
1298
|
case "bool":
|
|
@@ -1327,6 +1341,7 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
|
|
|
1327
1341
|
? `() { final v = ${reader}; return v == null ? null : ${s.refIdClass}.fromRow(v); }()`
|
|
1328
1342
|
: `${s.refIdClass}.fromRow(${reader})`;
|
|
1329
1343
|
}
|
|
1344
|
+
case "position":
|
|
1330
1345
|
case "string":
|
|
1331
1346
|
if (s.valueClass !== undefined) {
|
|
1332
1347
|
const reader = opt
|
|
@@ -1355,6 +1370,7 @@ function parseReadModelField(s: AggFieldSpec, aggregate: string): string {
|
|
|
1355
1370
|
: `readJsonObject(data, '${aggregate}', '${s.wireName}')`;
|
|
1356
1371
|
case "bool":
|
|
1357
1372
|
return opt ? `(data['${s.wireName}'] as bool?)` : `(data['${s.wireName}'] as bool)`;
|
|
1373
|
+
case "counter":
|
|
1358
1374
|
case "int":
|
|
1359
1375
|
return opt
|
|
1360
1376
|
? `readIntOrNull(data, '${aggregate}', '${s.wireName}')`
|
package/src/codegen_ts.ts
CHANGED
|
@@ -229,6 +229,12 @@ function tsTypeOfField(field: Field): string {
|
|
|
229
229
|
return "string";
|
|
230
230
|
case "int":
|
|
231
231
|
return "number";
|
|
232
|
+
// STRUCTURAL CRDT scalars (read-collapsed): a counter reads as its net number, a position as its
|
|
233
|
+
// order-key string (kernel `observed_value` → the projection surfaces a plain scalar).
|
|
234
|
+
case "counter":
|
|
235
|
+
return "number";
|
|
236
|
+
case "position":
|
|
237
|
+
return "string";
|
|
232
238
|
case "bool":
|
|
233
239
|
return "boolean";
|
|
234
240
|
case "enum":
|
package/src/directive.ts
CHANGED
|
@@ -53,11 +53,45 @@ export interface Directive<P = unknown> {
|
|
|
53
53
|
* directive, and the OBJECT it is checked on. The ONE gate runs `engine.check(schema, object,
|
|
54
54
|
* relation, principal)` over the pre-apply folded tuple store — refusing an author who does not
|
|
55
55
|
* hold it (the side-door fix made real). `object` is SELF-RELATIVE by default (`"workspace:self"`,
|
|
56
|
-
* the governance convention)
|
|
57
|
-
* the
|
|
58
|
-
*
|
|
56
|
+
* the governance convention). A PER-RECORD object (home_holons §6) is set via `objectFrom`: the gate
|
|
57
|
+
* then checks the relation on `<recordType>:<validated_payload[idField]>` — the specific record this
|
|
58
|
+
* directive writes, not the whole workspace (`record ⊂ object`; the engine is already object-generic).
|
|
59
|
+
* DISTINCT from the legacy `requiresCapability` (RBAC) and `declaredRelations` (cross-workspace) axes.
|
|
60
|
+
* Emitted into the `nomosRelationGate` USD era key; enforced IFF the law also declares a relation schema.
|
|
59
61
|
*/
|
|
60
|
-
readonly requiresRelationGate?: {
|
|
62
|
+
readonly requiresRelationGate?: {
|
|
63
|
+
readonly relation: string;
|
|
64
|
+
readonly object: string;
|
|
65
|
+
/** PER-RECORD (§6): when set, the gate object is `<recordType>:<payload[idField]>` (the static
|
|
66
|
+
* `object` is the fallback). `undefined` ⇒ the static object — era-ruled, byte-identical replay. */
|
|
67
|
+
readonly objectFrom?: { readonly recordType: string; readonly idField: string };
|
|
68
|
+
};
|
|
69
|
+
/**
|
|
70
|
+
* CROSS-CUSTODY ATTESTATION-AUTHORIZED (home_holons §3/§5). When `true`, this directive's authorization
|
|
71
|
+
* is a carried §3 SIGNED QUOTE (the asserter's pubkey + a signature over the edge), NOT an author block —
|
|
72
|
+
* so a key-less bailiff can deliver it host-free to an OFFLINE subject. Lowered into the
|
|
73
|
+
* `nomosAttestationGate` USD era key: the one gate then re-verifies the quote (bound to the home's warrant
|
|
74
|
+
* owner) AND pins the captured principal to the proven, unprivileged attestation key, and the signed-author
|
|
75
|
+
* doors exempt it. The directive's payload MUST carry the framework-convention fields the gate reads
|
|
76
|
+
* (`asserterWorkspace` / `object` / `relation` / `attestedAt` / `publicKey` / `signature` — the
|
|
77
|
+
* `recordShare` shape). FRAMEWORK-ONLY today (the home law's `recordShare`); a true E2E authority-free fact.
|
|
78
|
+
*/
|
|
79
|
+
readonly attestationAuthorizedGate?: {
|
|
80
|
+
/** The singleton aggregate INSTANCE id whose field holds the home owner (e.g. `home-owner:self`). */
|
|
81
|
+
readonly ownerObject: string;
|
|
82
|
+
/** The field on that singleton holding the owner principal (e.g. `principal`). */
|
|
83
|
+
readonly ownerField: string;
|
|
84
|
+
/** The payload field names the §3 quote rides in — the law's declared wire shape (defaulted to the
|
|
85
|
+
* framework `recordShare` convention by `.attestationAuthorized()`, so the kernel hardcodes none). */
|
|
86
|
+
readonly fields: {
|
|
87
|
+
readonly asserterWorkspace: string;
|
|
88
|
+
readonly object: string;
|
|
89
|
+
readonly relation: string;
|
|
90
|
+
readonly attestedAt: string;
|
|
91
|
+
readonly publicKey: string;
|
|
92
|
+
readonly signature: string;
|
|
93
|
+
};
|
|
94
|
+
};
|
|
61
95
|
/**
|
|
62
96
|
* The directive's DECLARED read boundary: the ref types its `plan` may READ (the
|
|
63
97
|
* "IR for law boundaries" decision — the boundary is part of the domain identity).
|
|
@@ -135,7 +169,17 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
135
169
|
* to the directive id) at the scope `scopeFrom(payload)` derives (defaults to root).
|
|
136
170
|
* Returns a new `Directive` carrying `requiresCapability` + `scopeFrom`.
|
|
137
171
|
*/
|
|
138
|
-
requires(
|
|
172
|
+
requires(
|
|
173
|
+
relation: string,
|
|
174
|
+
opts?: { object?: string; objectFrom?: { recordType: string; idField: string } },
|
|
175
|
+
): RequirableDirective<P>;
|
|
176
|
+
/**
|
|
177
|
+
* Declare this directive ATTESTATION-AUTHORIZED (home_holons §3/§5): its authorization is a carried §3
|
|
178
|
+
* signed quote rather than an author block — lowered into the `nomosAttestationGate` era key so the one
|
|
179
|
+
* gate verifies the quote + pins the principal to the attestation key, and the bailiff delivers it
|
|
180
|
+
* host-free. Framework-only (the home law's `recordShare`). Returns a NEW requirable directive.
|
|
181
|
+
*/
|
|
182
|
+
attestationAuthorized(owner: { ownerObject: string; ownerField: string }): RequirableDirective<P>;
|
|
139
183
|
/**
|
|
140
184
|
* Declare the directive's READ boundary. TWO argument kinds, one declaration site:
|
|
141
185
|
* * a STRING declares a ref TYPE its `plan` may read (the original boundary —
|
|
@@ -260,6 +304,19 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
260
304
|
}
|
|
261
305
|
}
|
|
262
306
|
|
|
307
|
+
/**
|
|
308
|
+
* Build a PER-RECORD authz object template (home_holons §6) for `.requires(rel, { objectFrom })`. The
|
|
309
|
+
* gate then checks the relation on `<Agg>:<validated_payload[idField]>` — the specific record this
|
|
310
|
+
* directive writes (e.g. `recordObject(Entry, "entryId")` ⇒ `Entry:<payload.entryId>`), not the whole
|
|
311
|
+
* workspace. `record ⊂ object`: the same engine, pointed at one record.
|
|
312
|
+
*/
|
|
313
|
+
export function recordObject(
|
|
314
|
+
agg: AggregateHandle,
|
|
315
|
+
idField: string,
|
|
316
|
+
): { recordType: string; idField: string } {
|
|
317
|
+
return { recordType: agg.id, idField };
|
|
318
|
+
}
|
|
319
|
+
|
|
263
320
|
/**
|
|
264
321
|
* Wrap a `Directive` with the chainable `.requires(...)` declaration. `.requires`
|
|
265
322
|
* is non-destructive: it returns a NEW requirable directive carrying the resolved
|
|
@@ -269,14 +326,46 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
269
326
|
function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
270
327
|
return {
|
|
271
328
|
...base,
|
|
272
|
-
requires(
|
|
329
|
+
requires(
|
|
330
|
+
relation: string,
|
|
331
|
+
opts?: { object?: string; objectFrom?: { recordType: string; idField: string } },
|
|
332
|
+
): RequirableDirective<P> {
|
|
273
333
|
// THE ONE write-authz declaration (full-native Zanzibar, no mixed systems): the captured author
|
|
274
|
-
// must HOLD `relation` on
|
|
275
|
-
//
|
|
276
|
-
//
|
|
334
|
+
// must HOLD `relation` on the object for the one gate to admit. Default object is self-relative
|
|
335
|
+
// (`"workspace:self"`, the governance convention); `objectFrom` (home_holons §6) gates on the
|
|
336
|
+
// PER-RECORD object the payload names (`recordObject(Agg, "idField")`) instead. Lowers to the
|
|
337
|
+
// `nomosRelationGate` era key; `admit` runs `engine.check`. (Replaces the deleted RBAC capability
|
|
338
|
+
// axis — a capability WAS just a relation.)
|
|
339
|
+
return makeRequirable({
|
|
340
|
+
...base,
|
|
341
|
+
requiresRelationGate: {
|
|
342
|
+
relation,
|
|
343
|
+
object: opts?.object ?? "workspace:self",
|
|
344
|
+
...(opts?.objectFrom !== undefined ? { objectFrom: opts.objectFrom } : {}),
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
},
|
|
348
|
+
attestationAuthorized(owner: { ownerObject: string; ownerField: string }): RequirableDirective<P> {
|
|
349
|
+
// CROSS-CUSTODY ATTESTATION-AUTHORIZED (home_holons §3/§5): this directive's authorization is a
|
|
350
|
+
// carried §3 signed quote, not an author block — lowered into the `nomosAttestationGate` era key, so
|
|
351
|
+
// the one gate verifies the quote (binding its subject to the LAW-DECLARED `owner`), pins the principal
|
|
352
|
+
// to the attestation key, and the signed-author doors exempt it. `owner` names the singleton + field
|
|
353
|
+
// holding the home owner; the QUOTE FIELD NAMES default to the framework `recordShare` convention HERE
|
|
354
|
+
// (in the DSL — the kernel reads them from the law's declaration, hardcoding nothing).
|
|
277
355
|
return makeRequirable({
|
|
278
356
|
...base,
|
|
279
|
-
|
|
357
|
+
attestationAuthorizedGate: {
|
|
358
|
+
ownerObject: owner.ownerObject,
|
|
359
|
+
ownerField: owner.ownerField,
|
|
360
|
+
fields: {
|
|
361
|
+
asserterWorkspace: "asserterWorkspace",
|
|
362
|
+
object: "object",
|
|
363
|
+
relation: "relation",
|
|
364
|
+
attestedAt: "attestedAt",
|
|
365
|
+
publicKey: "publicKey",
|
|
366
|
+
signature: "signature",
|
|
367
|
+
},
|
|
368
|
+
},
|
|
280
369
|
});
|
|
281
370
|
},
|
|
282
371
|
reads(...reads: (string | QueryDecl)[]): RequirableDirective<P> {
|
package/src/fields.ts
CHANGED
|
@@ -15,7 +15,7 @@
|
|
|
15
15
|
* the value type (via Zod) is preserved.
|
|
16
16
|
*/
|
|
17
17
|
import { z } from "zod";
|
|
18
|
-
import { Conflict, Lww, type Driver } from "./drivers.js";
|
|
18
|
+
import { Conflict, Counter, LastPosition, Lww, type Driver } from "./drivers.js";
|
|
19
19
|
import type { AggregateHandle } from "./aggregate.js";
|
|
20
20
|
|
|
21
21
|
export type FieldKind =
|
|
@@ -35,7 +35,14 @@ export type FieldKind =
|
|
|
35
35
|
// evidence.md §6/§9.2 — a content-addressed EvidenceRef. The migration target of the
|
|
36
36
|
// hand-wired `blobRef` `t.jsonObject()` string (thing_structures.ts). The authored
|
|
37
37
|
// value is the typed EvidenceRef shape; it encodes to a kernel `Value::Evidence`.
|
|
38
|
-
| "evidence"
|
|
38
|
+
| "evidence"
|
|
39
|
+
// ── STRUCTURAL CRDT scalar kinds (the read-collapsible drivers) ──
|
|
40
|
+
// `t.counter()` — a PN-counter (`Driver::Counter`): driven by `increment`/`decrement`, read as the
|
|
41
|
+
// net `Int` (Σpos − Σneg). `t.position()` — an LWW order-key (`Driver::LastPosition`): driven by
|
|
42
|
+
// `set`, read as the `String` key. Both collapse to a scalar at read time (kernel `observed_value`),
|
|
43
|
+
// so the read model + codegen treat them as `number` / `string`.
|
|
44
|
+
| "counter"
|
|
45
|
+
| "position";
|
|
39
46
|
|
|
40
47
|
/**
|
|
41
48
|
* The authored shape of an `EvidenceRef` (`evidence.md` §1) — a content-addressed
|
|
@@ -224,6 +231,21 @@ export const t = {
|
|
|
224
231
|
bool(): Field<boolean, "bool"> {
|
|
225
232
|
return makeField("bool", z.boolean(), Lww);
|
|
226
233
|
},
|
|
234
|
+
/** A PN-COUNTER (`Driver::Counter`) — a number that CONVERGES under concurrent edits: two replicas
|
|
235
|
+
* that each `increment` while offline both land (the merge is the per-replica sum), so nobody's count
|
|
236
|
+
* is lost. Drive it with `increment(handle, "field", n)` / `decrement(...)`, NOT `set()` (set would
|
|
237
|
+
* clobber the convergent state). Reads back as the net `number` (Σincrements − Σdecrements; may be
|
|
238
|
+
* negative). The kernel owns the CRDT; the field tag picks it. */
|
|
239
|
+
counter(): Field<number, "counter"> {
|
|
240
|
+
return makeField<number, "counter">("counter", z.number().int(), Counter);
|
|
241
|
+
},
|
|
242
|
+
/** An LWW ORDER-KEY POSITION (`Driver::LastPosition`) — an opaque fractional/index string (e.g. `"a0"`,
|
|
243
|
+
* `"a0V"`) that orders an item among siblings, resolved last-writer-wins by HLC. Drive it with
|
|
244
|
+
* `set(handle, "pos", key)`; concurrent moves converge to one position (the later HLC). Reads back as
|
|
245
|
+
* the `string` key. (Compute the key client-side — fractional indexing — and ride it in the payload.) */
|
|
246
|
+
position(): Field<string, "position"> {
|
|
247
|
+
return makeField<string, "position">("position", z.string(), LastPosition);
|
|
248
|
+
},
|
|
227
249
|
/** Enum via `as const` literal union — value typos squeal at compile time. */
|
|
228
250
|
enum<const E extends readonly [string, ...string[]]>(values: E): Field<E[number], "enum"> {
|
|
229
251
|
return makeField<E[number], "enum">(
|
|
@@ -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 QuickJS engine}.
|
|
4
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* framework/home.ts — THE HOME HOLON (the identity role; `architecture/home_holons_and_cross_custody.md`
|
|
8
|
+
* §4). One holon, three roles — "workspace," "platform," "home" are the SAME githolon wearing the role
|
|
9
|
+
* its installed LAW gives it. A HOME holon runs THIS law: an identity's subject-side root.
|
|
10
|
+
*
|
|
11
|
+
* It holds only PUBLIC material + signed facts (private keys never leave the client, so the cloud hosting
|
|
12
|
+
* it stays operator-blind):
|
|
13
|
+
* - KEY DIRECTORY — the identity's public device keys: the `signer` relation
|
|
14
|
+
* (`identity:<principal>#signer@key:<hash>`, `framework/signing_identity.ts`). Re-exported here so a
|
|
15
|
+
* home holon's law carries it: "wrap the scope key to U's device" becomes "read U's home key roster."
|
|
16
|
+
* - SHARED-WITH-ME index (§5) — one record per verified cross-custody attestation delivered here, so
|
|
17
|
+
* "show me everything shared with me" is a LOCAL query, no fan-out.
|
|
18
|
+
* - INBOX — grants/invites/notifications/dead-letters addressed to the identity.
|
|
19
|
+
*
|
|
20
|
+
* THE FORWARD-CHECK DOCTRINE (§5): the index is a DISCOVERY HINT, not the authority. When the identity
|
|
21
|
+
* actually opens the asserting workspace, THAT workspace's gate does the live `Check` — so a stale share
|
|
22
|
+
* (the asserter revoked it) is fail-safe: the identity sees a workspace it can no longer open, the forward
|
|
23
|
+
* check denies, the hint is pruned. (The cross-custody ATTESTATION that proves a delivered share is signed
|
|
24
|
+
* + verifiable is §3; the host DELIVERY lane that fills the index is §5. This file is the law that holds
|
|
25
|
+
* the result, and the owner-authored lane to maintain it.)
|
|
26
|
+
*/
|
|
27
|
+
import { z } from "zod";
|
|
28
|
+
import { aggregate, instance } from "../aggregate.js";
|
|
29
|
+
import { t } from "../fields.js";
|
|
30
|
+
import { Lww } from "../drivers.js";
|
|
31
|
+
import { directive } from "../directive.js";
|
|
32
|
+
import { create } from "../authoring.js";
|
|
33
|
+
import { set, type PlannedOp } from "../ops.js";
|
|
34
|
+
import { query } from "../query.js";
|
|
35
|
+
import { identityRelations } from "./signing_identity.js";
|
|
36
|
+
|
|
37
|
+
/** The KEY DIRECTORY is the `signer` relation — re-exported so a home holon's law composes it (the
|
|
38
|
+
* identity's public device keys, enrolled via `enrollSigner`, checked by the gate). */
|
|
39
|
+
export { identityRelations };
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* THE HOME OWNER (home_holons §5 — the owner stamp). A home holon is born UNWARRANTED (the subject may
|
|
43
|
+
* never have logged in — a share can be delivered to a stranger's lazily-born home, before they hold any
|
|
44
|
+
* device key, so there is no signed-author regime yet). This singleton records WHO the home belongs to: the
|
|
45
|
+
* §3 cross-custody gate binds a delivered quote's subject to THIS principal (`executor::HOME_OWNER_ID`), so
|
|
46
|
+
* a quote signed for anyone else fails closed. Seeded ONCE at birth by the host (the control-plane
|
|
47
|
+
* `seedHomeOwner`, kept off the open session lane), so a tenant cannot forge ownership.
|
|
48
|
+
*/
|
|
49
|
+
export const HOME_OWNER_ID = "home-owner:self";
|
|
50
|
+
export const HomeOwner = aggregate("HomeOwner", {
|
|
51
|
+
/** The identity this home belongs to — the canonical `user:<uid>` subject the §3 gate binds quotes to. */
|
|
52
|
+
principal: t.string().merge(Lww),
|
|
53
|
+
/** ISO-8601, caller-stamped (determinism — no clock in the plan). */
|
|
54
|
+
bornAt: t.string().merge(Lww),
|
|
55
|
+
});
|
|
56
|
+
|
|
57
|
+
/** The framework PRIMITIVE plan: stamp the home's owner (singleton at the fixed id the gate reads). */
|
|
58
|
+
export function writeHomeOwner(p: { principal: string; bornAt: string }): PlannedOp[] {
|
|
59
|
+
const h = instance(HomeOwner, HOME_OWNER_ID);
|
|
60
|
+
return [set(h, "principal", p.principal), set(h, "bornAt", p.bornAt)];
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* `seedHomeOwner` — the birth-door directive that stamps the home's owner at the fixed singleton id. A
|
|
65
|
+
* CONTROL-PLANE write: the host authors it once at lazy birth (after resolving who the home is for); the
|
|
66
|
+
* open session lane structurally REFUSES it (by directive id, like the §6 handoff seals), so a tenant
|
|
67
|
+
* cannot overwrite ownership. `.ensures` (a deterministic-keyed singleton, not a minted record).
|
|
68
|
+
*/
|
|
69
|
+
export const seedHomeOwner = directive("seedHomeOwner")
|
|
70
|
+
.ensures(HomeOwner)
|
|
71
|
+
.payload(z.object({ principal: z.string().min(1), bornAt: z.string().min(1) }))
|
|
72
|
+
.plan((p) => writeHomeOwner(p));
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* THE SHARED-WITH-ME INDEX (§5) — one record per cross-custody attestation delivered to this home holon:
|
|
76
|
+
* a signed edge `<asserterWorkspace> : <object> # <relation> @ this-identity`, indexed LOCALLY so the
|
|
77
|
+
* access list is a local query (no fan-out across every resource holon). A DISCOVERY HINT — the live authz
|
|
78
|
+
* stays the forward `Check` on the asserting holon when the identity opens it.
|
|
79
|
+
*/
|
|
80
|
+
export const SharedWithMe = aggregate("SharedWithMe", {
|
|
81
|
+
/** The holon that signed + delivered this edge — WHERE the live forward `Check` runs when opened. */
|
|
82
|
+
asserterWorkspace: t.string().merge(Lww),
|
|
83
|
+
/** The shared OBJECT — the whole workspace (`workspace:self`) or a specific record (`Entry:<id>`). */
|
|
84
|
+
object: t.string().merge(Lww),
|
|
85
|
+
/** The relation granted (`reader` / `editor` / a read cap). */
|
|
86
|
+
relation: t.string().merge(Lww),
|
|
87
|
+
/** The asserter's OWNER principal — whose key signed the attestation (verified at §3 admission). */
|
|
88
|
+
asserterPrincipal: t.string().merge(Lww),
|
|
89
|
+
/** ISO-8601, caller-stamped (determinism — no clock in the plan). */
|
|
90
|
+
attestedAt: t.string().merge(Lww),
|
|
91
|
+
/** PRUNED (forward-check fail-safe): set when the live `Check` on the asserter denies — the hint is
|
|
92
|
+
* dropped from the list but the audit row stays. `""` = live. */
|
|
93
|
+
prunedAt: t.string().merge(Lww),
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* `recordShare` — index a cross-custody share into the home holon's access list. AUTHORED on the home
|
|
98
|
+
* holon (the identity's own subject-side root). The payload carries the §3 CROSS-CUSTODY ATTESTATION — a
|
|
99
|
+
* SIGNED QUOTE: the asserter's device `publicKey` + a `signature` over the edge
|
|
100
|
+
* `<asserterWorkspace>:<object>#<relation>@<this-home-owner>`. The home holon's OWN gate verifies it
|
|
101
|
+
* (`executor::record_share_attestation_violation`, bound to the warrant owner), so the BAILIFF can deliver
|
|
102
|
+
* it HOST-FREE (the host carries bytes, vouches nothing — the gate vouches). The owner lane authors the
|
|
103
|
+
* SAME shape (the owner holds the quote the asserter gave it). The signature is a verification input, not
|
|
104
|
+
* stored — only the edge facts fold into the index. The live authority stays the forward `Check` on the
|
|
105
|
+
* asserter when the identity opens it (§5 fail-safe); a stale/forged quote can't enter the list at all.
|
|
106
|
+
*/
|
|
107
|
+
export const recordShare = directive("recordShare")
|
|
108
|
+
.creates(SharedWithMe)
|
|
109
|
+
.payload(
|
|
110
|
+
z.object({
|
|
111
|
+
asserterWorkspace: z.string().min(1),
|
|
112
|
+
object: z.string().min(1),
|
|
113
|
+
relation: z.string().min(1),
|
|
114
|
+
asserterPrincipal: z.string().min(1),
|
|
115
|
+
attestedAt: z.string().min(1),
|
|
116
|
+
/** The asserter's device PUBLIC KEY (base64) — what the §3 gate verifies the quote against. */
|
|
117
|
+
publicKey: z.string().min(1),
|
|
118
|
+
/** The asserter's SIGNATURE (base64) over the edge bytes, subject = this home's owner. */
|
|
119
|
+
signature: z.string().min(1),
|
|
120
|
+
}),
|
|
121
|
+
)
|
|
122
|
+
.plan((p) => {
|
|
123
|
+
create(SharedWithMe)
|
|
124
|
+
.set("asserterWorkspace", p.asserterWorkspace)
|
|
125
|
+
.set("object", p.object)
|
|
126
|
+
.set("relation", p.relation)
|
|
127
|
+
.set("asserterPrincipal", p.asserterPrincipal)
|
|
128
|
+
.set("attestedAt", p.attestedAt)
|
|
129
|
+
.set("prunedAt", "");
|
|
130
|
+
return [];
|
|
131
|
+
})
|
|
132
|
+
// CROSS-CUSTODY ATTESTATION-AUTHORIZED (§3/§5): the payload's signed quote IS the authorization (lowered
|
|
133
|
+
// into the `nomosAttestationGate` era key) — the one gate verifies it against the home's owner (the
|
|
134
|
+
// LAW-DECLARED owner-source below, NOT a kernel-hardcoded id) + pins the principal to the attestation key,
|
|
135
|
+
// so the bailiff delivers it host-free to an OFFLINE subject (no device key, no author block).
|
|
136
|
+
.attestationAuthorized({ ownerObject: HOME_OWNER_ID, ownerField: "principal" });
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* `pruneShare` — fail-safe drop of a hint whose forward `Check` denied (the asserter revoked it). The row
|
|
140
|
+
* stays for audit; `prunedAt` removes it from the live list. (The forward check itself is the resource
|
|
141
|
+
* holon's job — this records the verdict locally.)
|
|
142
|
+
*/
|
|
143
|
+
export const pruneShare = directive("pruneShare")
|
|
144
|
+
.mutates(SharedWithMe)
|
|
145
|
+
.payload(z.object({ shareId: z.string().min(1), prunedAt: z.string().min(1) }))
|
|
146
|
+
.plan((p) => [set(instance(SharedWithMe, p.shareId), "prunedAt", p.prunedAt)]);
|
|
147
|
+
|
|
148
|
+
/** "Show me everything <asserter> shared with me" — an indexed probe over the local access list. */
|
|
149
|
+
export const sharedByWorkspace = query("sharedByWorkspace").key("asserterWorkspace").returns(SharedWithMe);
|
package/src/index.ts
CHANGED
|
@@ -51,6 +51,9 @@ export {
|
|
|
51
51
|
set,
|
|
52
52
|
addToSet,
|
|
53
53
|
setEntry,
|
|
54
|
+
increment,
|
|
55
|
+
decrement,
|
|
56
|
+
removeFromSet,
|
|
54
57
|
withMarker,
|
|
55
58
|
strike,
|
|
56
59
|
type PlannedOp,
|
|
@@ -58,7 +61,7 @@ export {
|
|
|
58
61
|
type PlannedEventMarker,
|
|
59
62
|
type StrikeOp,
|
|
60
63
|
} from "./ops.js";
|
|
61
|
-
export { directive, type Directive, type ReferentialMarker } from "./directive.js";
|
|
64
|
+
export { directive, recordObject, type Directive, type ReferentialMarker } from "./directive.js";
|
|
62
65
|
// FIRST-CLASS WORKSPACE TYPES (sharding §10 RATIFIED, slice 1) live on the SUBPATH
|
|
63
66
|
// `@githolon/dsl/workspace-type` (`workspaceType("estate").root(Estate).hasMany(...)`),
|
|
64
67
|
// NOT this runtime barrel — the barrel is bundled into EVERY tenant's engine lump, and
|
|
@@ -231,6 +234,18 @@ export {
|
|
|
231
234
|
SIGNER_RELATION,
|
|
232
235
|
enrollSigner,
|
|
233
236
|
} from "./framework/signing_identity.js";
|
|
237
|
+
// THE HOME HOLON (home_holons_and_cross_custody.md §4) — the identity-role law: the shared-with-me access
|
|
238
|
+
// index + the owner lane to maintain it (the key directory is the `signer` relation above).
|
|
239
|
+
export {
|
|
240
|
+
SharedWithMe,
|
|
241
|
+
recordShare,
|
|
242
|
+
pruneShare,
|
|
243
|
+
sharedByWorkspace,
|
|
244
|
+
HomeOwner,
|
|
245
|
+
HOME_OWNER_ID,
|
|
246
|
+
seedHomeOwner,
|
|
247
|
+
writeHomeOwner,
|
|
248
|
+
} from "./framework/home.js";
|
|
234
249
|
// THE GENESIS WARRANT (identity_keys.md step 0) — the in-history birth fact that lets
|
|
235
250
|
// the gate's genesis clause admit a founding control-plane grant on a virgin chain.
|
|
236
251
|
export {
|
package/src/ops.ts
CHANGED
|
@@ -37,7 +37,7 @@ export interface FieldOp {
|
|
|
37
37
|
*/
|
|
38
38
|
readonly marker?: PlannedEventMarker;
|
|
39
39
|
readonly field: string;
|
|
40
|
-
readonly kind: "set" | "addToSet" | "setEntry";
|
|
40
|
+
readonly kind: "set" | "addToSet" | "setEntry" | "increment" | "decrement" | "removeFromSet";
|
|
41
41
|
readonly value?: unknown;
|
|
42
42
|
readonly items?: readonly string[];
|
|
43
43
|
readonly entryKey?: string;
|
|
@@ -147,6 +147,55 @@ export function addToSet<
|
|
|
147
147
|
return { ...addressOf(agg), field, kind: "addToSet", items };
|
|
148
148
|
}
|
|
149
149
|
|
|
150
|
+
/**
|
|
151
|
+
* `increment(handle, "field", amount)` — add to a PN-COUNTER field (`t.counter()`). The ONLY way to
|
|
152
|
+
* advance a counter (set() would clobber the convergent CRDT state). Concurrent increments on different
|
|
153
|
+
* replicas all land (the merge sums the per-replica totals), so nobody's count is lost. `amount` is a
|
|
154
|
+
* non-negative integer; the field must be `kind:"counter"`. `decrement` is the symmetric subtract.
|
|
155
|
+
*/
|
|
156
|
+
function counterDelta<A extends Target, K extends CounterKey<A>>(
|
|
157
|
+
agg: A,
|
|
158
|
+
field: K,
|
|
159
|
+
amount: number,
|
|
160
|
+
kind: "increment" | "decrement",
|
|
161
|
+
): FieldOp {
|
|
162
|
+
if (!Number.isInteger(amount) || amount < 0) {
|
|
163
|
+
throw new Error(`${kind}('${String(field)}', ${amount}): amount must be a non-negative integer.`);
|
|
164
|
+
}
|
|
165
|
+
return { ...addressOf(agg), field: field as string, kind, value: amount };
|
|
166
|
+
}
|
|
167
|
+
/** A counter field key — `kind:"counter"` only (an `int` is NOT a counter; that would not converge). */
|
|
168
|
+
type CounterKey<A> = {
|
|
169
|
+
[P in keyof Fields<A>]: Fields<A>[P] extends { kind: "counter" } ? P : never;
|
|
170
|
+
}[keyof Fields<A>] &
|
|
171
|
+
string;
|
|
172
|
+
|
|
173
|
+
export function increment<A extends Target, K extends CounterKey<A>>(agg: A, field: K, amount = 1): FieldOp {
|
|
174
|
+
return counterDelta(agg, field, amount, "increment");
|
|
175
|
+
}
|
|
176
|
+
/** `decrement(handle, "field", amount)` — subtract from a PN-counter field. See {@link increment}. */
|
|
177
|
+
export function decrement<A extends Target, K extends CounterKey<A>>(agg: A, field: K, amount = 1): FieldOp {
|
|
178
|
+
return counterDelta(agg, field, amount, "decrement");
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
/**
|
|
182
|
+
* `removeFromSet(handle, "field", items)` — tombstone items in a REMOVE-WINS set (`t.set(...).merge(
|
|
183
|
+
* RemoveWins)`). The remove permanently WINS over any concurrent or later add of the same element
|
|
184
|
+
* (2-phase-set semantics), so a concurrent add+remove converges to "removed". The additive partner is
|
|
185
|
+
* {@link addToSet}; on a RemoveWins field both are encoded to the kernel's 2-phase ops. (On a plain
|
|
186
|
+
* AddWins set there is no remove — that's the whole point of AddWins — so this targets set fields and
|
|
187
|
+
* the encoder refuses a non-RemoveWins set at compile output.)
|
|
188
|
+
*/
|
|
189
|
+
export function removeFromSet<
|
|
190
|
+
A extends Target,
|
|
191
|
+
K extends {
|
|
192
|
+
[P in keyof Fields<A>]: FieldValue<Fields<A>[P]> extends string[] ? P : never;
|
|
193
|
+
}[keyof Fields<A>] &
|
|
194
|
+
string,
|
|
195
|
+
>(agg: A, field: K, items: readonly string[]): FieldOp {
|
|
196
|
+
return { ...addressOf(agg), field, kind: "removeFromSet", items };
|
|
197
|
+
}
|
|
198
|
+
|
|
150
199
|
/** Set one entry of a map field. The map's value type is checked. */
|
|
151
200
|
export function setEntry<
|
|
152
201
|
A extends Target,
|
package/src/usd.ts
CHANGED
|
@@ -221,6 +221,31 @@ export interface UsdLayer {
|
|
|
221
221
|
readonly v: 1;
|
|
222
222
|
readonly requires: Record<string, { readonly relation: string; readonly object: string }>;
|
|
223
223
|
};
|
|
224
|
+
/**
|
|
225
|
+
* THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5). Present IFF ≥1 directive declares
|
|
226
|
+
* `.attestationAuthorized()`: the SORTED set of their ids. A directive named here is authorized by a
|
|
227
|
+
* carried §3 signed quote rather than an author block — the one gate verifies the quote (bound to the
|
|
228
|
+
* home's warrant owner) + pins the captured principal to the proven attestation key, and the signed-author
|
|
229
|
+
* doors exempt it. OMITTED for law with no such directive (the era rule — byte-identical hash).
|
|
230
|
+
*/
|
|
231
|
+
readonly nomosAttestationGate?: {
|
|
232
|
+
readonly v: 1;
|
|
233
|
+
readonly directives: Record<
|
|
234
|
+
string,
|
|
235
|
+
{
|
|
236
|
+
readonly ownerObject: string;
|
|
237
|
+
readonly ownerField: string;
|
|
238
|
+
readonly fields: {
|
|
239
|
+
readonly asserterWorkspace: string;
|
|
240
|
+
readonly object: string;
|
|
241
|
+
readonly relation: string;
|
|
242
|
+
readonly attestedAt: string;
|
|
243
|
+
readonly publicKey: string;
|
|
244
|
+
readonly signature: string;
|
|
245
|
+
};
|
|
246
|
+
}
|
|
247
|
+
>;
|
|
248
|
+
};
|
|
224
249
|
/**
|
|
225
250
|
* STABLE IDENTIFIERS (#58 — names are labels, identity is minted). The layer's
|
|
226
251
|
* domain/aggregate/field sids + label lineages, carried verbatim from the canonical
|
|
@@ -448,14 +473,48 @@ export function emitUsd(
|
|
|
448
473
|
]);
|
|
449
474
|
// THE RELATION GATE (P4): directives declaring `.requiresRelation` → directiveId → {relation,
|
|
450
475
|
// object}. SORTED keys; omit-when-empty (era rule — a relation-gate-free layer is byte-identical).
|
|
451
|
-
const relationGateReq: Record<
|
|
476
|
+
const relationGateReq: Record<
|
|
477
|
+
string,
|
|
478
|
+
{ relation: string; object: string; objectFrom?: { recordType: string; idField: string } }
|
|
479
|
+
> = {};
|
|
452
480
|
for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
453
481
|
if (d.requiresRelationGate !== undefined) {
|
|
454
|
-
relationGateReq[d.id] = {
|
|
482
|
+
relationGateReq[d.id] = {
|
|
483
|
+
relation: d.requiresRelationGate.relation,
|
|
484
|
+
object: d.requiresRelationGate.object,
|
|
485
|
+
// PER-RECORD object (home_holons §6): carried through to the wasm gate; omitted when absent
|
|
486
|
+
// so a self-relative directive's USD is byte-identical (the era rule).
|
|
487
|
+
...(d.requiresRelationGate.objectFrom !== undefined
|
|
488
|
+
? { objectFrom: d.requiresRelationGate.objectFrom }
|
|
489
|
+
: {}),
|
|
490
|
+
};
|
|
455
491
|
}
|
|
456
492
|
}
|
|
457
493
|
const nomosRelationGate =
|
|
458
494
|
Object.keys(relationGateReq).length > 0 ? { v: 1 as const, requires: relationGateReq } : undefined;
|
|
495
|
+
// THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5): directives declaring `.attestationAuthorized()`
|
|
496
|
+
// → directiveId → its LAW-DECLARED owner-source {ownerObject, ownerField}. SORTED keys (byte-
|
|
497
|
+
// determinism). Omit-when-empty (era rule). The one gate verifies the carried §3 quote (binding its
|
|
498
|
+
// subject to the named owner) + pins the principal to the attestation key — the kernel hardcodes no id.
|
|
499
|
+
const attestationReq: Record<
|
|
500
|
+
string,
|
|
501
|
+
{
|
|
502
|
+
ownerObject: string;
|
|
503
|
+
ownerField: string;
|
|
504
|
+
fields: { asserterWorkspace: string; object: string; relation: string; attestedAt: string; publicKey: string; signature: string };
|
|
505
|
+
}
|
|
506
|
+
> = {};
|
|
507
|
+
for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
508
|
+
if (d.attestationAuthorizedGate !== undefined) {
|
|
509
|
+
attestationReq[d.id] = {
|
|
510
|
+
ownerObject: d.attestationAuthorizedGate.ownerObject,
|
|
511
|
+
ownerField: d.attestationAuthorizedGate.ownerField,
|
|
512
|
+
fields: { ...d.attestationAuthorizedGate.fields },
|
|
513
|
+
};
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
const nomosAttestationGate =
|
|
517
|
+
Object.keys(attestationReq).length > 0 ? { v: 1 as const, directives: attestationReq } : undefined;
|
|
459
518
|
// THE CAPTURED-READ GATE DECLARATION (#58): collected from the canonical
|
|
460
519
|
// manifest's per-directive `capturedReads` (omit-when-empty — read-free law's
|
|
461
520
|
// USD is byte-identical apart from the always-on stableIds key).
|
|
@@ -515,6 +574,7 @@ export function emitUsd(
|
|
|
515
574
|
: {}),
|
|
516
575
|
...(nomosRelations !== undefined ? { nomosRelations } : {}),
|
|
517
576
|
...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
|
|
577
|
+
...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
|
|
518
578
|
...(Object.keys(visAggregates).length > 0
|
|
519
579
|
? { nomosReadVisibility: { v: 1 as const, aggregates: visAggregates } }
|
|
520
580
|
: {}),
|
package/src/usd_layers.ts
CHANGED
|
@@ -371,6 +371,9 @@ export function usdaLayerText(
|
|
|
371
371
|
...(layer.nomosRelationGate !== undefined
|
|
372
372
|
? [` custom string nomos:relationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosRelationGate)))}`]
|
|
373
373
|
: []),
|
|
374
|
+
...(layer.nomosAttestationGate !== undefined
|
|
375
|
+
? [` custom string nomos:attestationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestationGate)))}`]
|
|
376
|
+
: []),
|
|
374
377
|
``,
|
|
375
378
|
...body,
|
|
376
379
|
` }`,
|
|
@@ -584,6 +587,22 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
584
587
|
relationGate = { v: 1, requires: requiresRec };
|
|
585
588
|
}
|
|
586
589
|
}
|
|
590
|
+
// THE ATTESTATION GATE (home_holons §3/§5): directiveId → owner-source merged later-wins across layers,
|
|
591
|
+
// SORTED keys. Omit-when-empty (era rule).
|
|
592
|
+
let attestationGate: UsdLayer["nomosAttestationGate"];
|
|
593
|
+
{
|
|
594
|
+
type AttSrc = NonNullable<UsdLayer["nomosAttestationGate"]>["directives"][string];
|
|
595
|
+
const a = new Map<string, AttSrc>();
|
|
596
|
+
for (const l of stack) {
|
|
597
|
+
if (l.nomosAttestationGate === undefined) continue;
|
|
598
|
+
for (const [id, src] of Object.entries(l.nomosAttestationGate.directives)) a.set(id, src);
|
|
599
|
+
}
|
|
600
|
+
if (a.size > 0) {
|
|
601
|
+
const rec: Record<string, AttSrc> = {};
|
|
602
|
+
for (const k of [...a.keys()].sort()) rec[k] = a.get(k)!;
|
|
603
|
+
attestationGate = { v: 1, directives: rec };
|
|
604
|
+
}
|
|
605
|
+
}
|
|
587
606
|
return {
|
|
588
607
|
path,
|
|
589
608
|
prims: [...prims.keys()].sort().map((p) => prims.get(p)!),
|
|
@@ -596,6 +615,7 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
596
615
|
...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
|
|
597
616
|
...(relations !== undefined ? { nomosRelations: relations } : {}),
|
|
598
617
|
...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
|
|
618
|
+
...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
|
|
599
619
|
};
|
|
600
620
|
});
|
|
601
621
|
|
|
@@ -719,6 +739,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
719
739
|
const encryptionByDomain = new Map<string, UsdLayer["nomosEncryption"]>();
|
|
720
740
|
const relationsByDomain = new Map<string, UsdLayer["nomosRelations"]>();
|
|
721
741
|
const relationGateByDomain = new Map<string, UsdLayer["nomosRelationGate"]>();
|
|
742
|
+
const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
|
|
722
743
|
for (const p of prims) {
|
|
723
744
|
if (p.attrs.get("nomos:kind") === "domain") {
|
|
724
745
|
const domain = attrString(p, "nomos:domain");
|
|
@@ -752,6 +773,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
752
773
|
if (typeof relGateHex === "string") {
|
|
753
774
|
relationGateByDomain.set(domain, JSON.parse(unhexUtf8(relGateHex)) as UsdLayer["nomosRelationGate"]);
|
|
754
775
|
}
|
|
776
|
+
// THE ATTESTATION GATE (home_holons §3/§5): the hex-carried attestation-authorized directive set.
|
|
777
|
+
const attGateHex = p.attrs.get("nomos:attestationGateHex");
|
|
778
|
+
if (typeof attGateHex === "string") {
|
|
779
|
+
attestationGateByDomain.set(domain, JSON.parse(unhexUtf8(attGateHex)) as UsdLayer["nomosAttestationGate"]);
|
|
780
|
+
}
|
|
755
781
|
}
|
|
756
782
|
}
|
|
757
783
|
|
|
@@ -845,6 +871,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
845
871
|
const enc = encryptionByDomain.get(domain);
|
|
846
872
|
const rel = relationsByDomain.get(domain);
|
|
847
873
|
const relGate = relationGateByDomain.get(domain);
|
|
874
|
+
const attGate = attestationGateByDomain.get(domain);
|
|
848
875
|
return {
|
|
849
876
|
path: `/Nomos/${domain}`,
|
|
850
877
|
prims: [...b.prims].sort(byPath),
|
|
@@ -857,6 +884,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
857
884
|
...(enc !== undefined ? { nomosEncryption: enc } : {}),
|
|
858
885
|
...(rel !== undefined ? { nomosRelations: rel } : {}),
|
|
859
886
|
...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
|
|
887
|
+
...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
|
|
860
888
|
};
|
|
861
889
|
})
|
|
862
890
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
package/src/wire.ts
CHANGED
|
@@ -39,7 +39,12 @@ export type WireValue =
|
|
|
39
39
|
| { Int: number }
|
|
40
40
|
| { Set: string[] }
|
|
41
41
|
| { Map: Record<string, WireField> }
|
|
42
|
-
| { Evidence: WireEvidenceRef }
|
|
42
|
+
| { Evidence: WireEvidenceRef }
|
|
43
|
+
// ── STRUCTURAL CRDT state values (only seen when reading RAW folded state back; the read MODEL
|
|
44
|
+
// collapses them via the kernel `observed_value` to Int/Str/Set, so typed reads stay scalar) ──
|
|
45
|
+
| { Counter: Record<string, { pos: number; neg: number }> }
|
|
46
|
+
| { Position: string }
|
|
47
|
+
| { TwoPhaseSet: { adds: string[]; removes: string[] } };
|
|
43
48
|
|
|
44
49
|
/** `EvidenceRef { hash: ContentHash, media_type, byte_length, storage_class }` — the
|
|
45
50
|
* kernel serde shape. `ContentHash(String)` serializes transparently as a bare string
|
|
@@ -57,11 +62,21 @@ export interface WireField {
|
|
|
57
62
|
stamp: WireHlc;
|
|
58
63
|
}
|
|
59
64
|
|
|
60
|
-
/**
|
|
65
|
+
/**
|
|
66
|
+
* `Op` — the kernel's externally-tagged serde shape (`nomos2/kernel/src/lib.rs::Op`). The originals
|
|
67
|
+
* (Set / AddToSet / SetEntry) plus the STRUCTURAL CRDT ops: `AddToTwoPhase`/`RemoveFromTwoPhase`
|
|
68
|
+
* (the `removeWins` 2-phase set), `IncrementCounter`/`DecrementCounter` (the `counter` PN-counter,
|
|
69
|
+
* carrying a bare `u64` amount), and `SetPosition` (the `lastPosition` LWW order-key, a bare string).
|
|
70
|
+
*/
|
|
61
71
|
export type WireOp =
|
|
62
72
|
| { Set: WireValue }
|
|
63
73
|
| { AddToSet: string[] }
|
|
64
|
-
| { SetEntry: { key: string; value: WireValue } }
|
|
74
|
+
| { SetEntry: { key: string; value: WireValue } }
|
|
75
|
+
| { AddToTwoPhase: string[] }
|
|
76
|
+
| { RemoveFromTwoPhase: string[] }
|
|
77
|
+
| { IncrementCounter: number }
|
|
78
|
+
| { DecrementCounter: number }
|
|
79
|
+
| { SetPosition: string };
|
|
65
80
|
|
|
66
81
|
/** `FieldOp { field: FieldId, op: Op }`. */
|
|
67
82
|
export interface WireFieldOp {
|
package/src/wire_encode.ts
CHANGED
|
@@ -100,13 +100,39 @@ function encodeKernelValue(field: Field | undefined, value: unknown): WireValue
|
|
|
100
100
|
}
|
|
101
101
|
|
|
102
102
|
function encodeKernelFieldOp(op: FieldOp, field: Field | undefined): WireFieldOp {
|
|
103
|
+
// The field's DRIVER selects the kernel op for the structural CRDTs — a `set`/`addToSet` on a
|
|
104
|
+
// position / remove-wins field lowers to `SetPosition` / `AddToTwoPhase`, never the scalar/AddWins op.
|
|
105
|
+
const driver = field?.driver?.kind;
|
|
103
106
|
let wireOp: WireOp;
|
|
104
107
|
switch (op.kind) {
|
|
105
108
|
case "set":
|
|
106
|
-
|
|
109
|
+
// A `lastPosition` field's `set` is the kernel `SetPosition` (LWW order-key, a bare string).
|
|
110
|
+
wireOp =
|
|
111
|
+
driver === "LastPosition"
|
|
112
|
+
? { SetPosition: String(op.value) }
|
|
113
|
+
: { Set: encodeKernelValue(field, op.value) };
|
|
107
114
|
break;
|
|
108
115
|
case "addToSet":
|
|
109
|
-
|
|
116
|
+
// On a remove-wins set the add grows the 2-phase set's `adds`; on AddWins it is the plain union.
|
|
117
|
+
wireOp =
|
|
118
|
+
driver === "RemoveWins"
|
|
119
|
+
? { AddToTwoPhase: [...(op.items ?? [])] }
|
|
120
|
+
: { AddToSet: [...(op.items ?? [])] };
|
|
121
|
+
break;
|
|
122
|
+
case "removeFromSet":
|
|
123
|
+
if (driver !== "RemoveWins") {
|
|
124
|
+
throw new Error(
|
|
125
|
+
`removeFromSet('${op.field}'): only a RemoveWins set has a remove (an AddWins set is ` +
|
|
126
|
+
`add-only by design). Declare the field \`t.set(...).merge(RemoveWins)\`.`,
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
wireOp = { RemoveFromTwoPhase: [...(op.items ?? [])] };
|
|
130
|
+
break;
|
|
131
|
+
case "increment":
|
|
132
|
+
wireOp = { IncrementCounter: Number(op.value) };
|
|
133
|
+
break;
|
|
134
|
+
case "decrement":
|
|
135
|
+
wireOp = { DecrementCounter: Number(op.value) };
|
|
110
136
|
break;
|
|
111
137
|
case "setEntry":
|
|
112
138
|
wireOp = {
|