@githolon/dsl 0.55.1 → 0.56.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/dart/pubspec.yaml +1 -1
- package/package.json +1 -1
- package/src/attested.ts +9 -0
- package/src/attested_core.ts +199 -2
- package/src/build_package.ts +17 -1
- package/src/codegen_dart.ts +7 -0
- package/src/codegen_proof.ts +31 -1
- package/src/directive.ts +55 -11
- package/src/index.ts +6 -0
- package/src/law_check.ts +107 -0
- package/src/read.ts +5 -2
- package/src/shape_compat.ts +10 -6
- package/src/usd.ts +145 -26
- package/src/usd_layers.ts +16 -4
package/dart/pubspec.yaml
CHANGED
|
@@ -11,7 +11,7 @@ description: >-
|
|
|
11
11
|
Nomos 2 Dart frontend types — the hand-written kernel wire types plus the
|
|
12
12
|
code-generated per-domain aggregate + directive-payload types. (De)serializes
|
|
13
13
|
the EXACT serde JSON the Rust kernel + projection produce/consume.
|
|
14
|
-
version: 0.
|
|
14
|
+
version: 0.35.0
|
|
15
15
|
|
|
16
16
|
environment:
|
|
17
17
|
sdk: ^3.5.0
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.56.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/attested.ts
CHANGED
|
@@ -32,12 +32,21 @@ export {
|
|
|
32
32
|
PARENT_TRUST_SENTINEL,
|
|
33
33
|
foreignQuery,
|
|
34
34
|
isTrustSourceDecl,
|
|
35
|
+
trustPolicy,
|
|
36
|
+
isTrustPolicyDecl,
|
|
37
|
+
fromPayload,
|
|
38
|
+
isFromPayloadRef,
|
|
35
39
|
NomosSourceFrontier,
|
|
36
40
|
sourceFrontierId,
|
|
37
41
|
SOURCE_FRONTIER_PREFIX,
|
|
38
42
|
__trustFor,
|
|
43
|
+
__policyFor,
|
|
39
44
|
type TrustSourceDecl,
|
|
40
45
|
type TrustSourceOptions,
|
|
46
|
+
type TrustPolicyDecl,
|
|
47
|
+
type TrustPolicyOptions,
|
|
48
|
+
type FromPayloadRef,
|
|
49
|
+
type ForeignReadEdge,
|
|
41
50
|
type ForeignQueryRef,
|
|
42
51
|
} from "./attested_core.js";
|
|
43
52
|
|
package/src/attested_core.ts
CHANGED
|
@@ -10,8 +10,8 @@
|
|
|
10
10
|
* module-init cycle. The sanctioning ensure directive + the merge-seam injector live in
|
|
11
11
|
* `attested.ts`. Full doctrine: `architecture/attested_reads_design.md`.
|
|
12
12
|
*/
|
|
13
|
-
import { aggregate } from "./aggregate.js";
|
|
14
|
-
import { t } from "./fields.js";
|
|
13
|
+
import { aggregate, type AggregateHandle } from "./aggregate.js";
|
|
14
|
+
import { t, type Field } from "./fields.js";
|
|
15
15
|
import { Lww } from "./drivers.js";
|
|
16
16
|
|
|
17
17
|
/** A lightweight reference to a query declared in the SOURCE workspace's law — the reader
|
|
@@ -125,6 +125,203 @@ export function isTrustSourceDecl(v: unknown): v is TrustSourceDecl {
|
|
|
125
125
|
);
|
|
126
126
|
}
|
|
127
127
|
|
|
128
|
+
// ── DYNAMIC-SOURCE trust: a policy over a LAW-STATE allow-list (dynamic_source_attested_reads.md) ──
|
|
129
|
+
// The static `trustSource("ws", …)` edge names a FIXED workspace; a `trustPolicy` instead names a
|
|
130
|
+
// reader-side aggregate whose folded rows ARE the acceptable-source set — so the routed workspace is
|
|
131
|
+
// chosen at RUNTIME (the payload names it) while the trust judgment (genesis + accepted keys) stays a
|
|
132
|
+
// deterministic fold of the reader's own gated-enrolment writes. The gate resolves `att.source_genesis`
|
|
133
|
+
// against the allow-list rows; the NAME is routing, the genesis + keys are identity.
|
|
134
|
+
|
|
135
|
+
/** A marker naming a PAYLOAD FIELD that carries the routed source-workspace name at runtime — the
|
|
136
|
+
* first argument of the dynamic `.readsFrom(fromPayload("field"), query, { policy })` overload. */
|
|
137
|
+
export interface FromPayloadRef {
|
|
138
|
+
readonly payloadField: string;
|
|
139
|
+
/** brand — distinguishes the dynamic (payload-routed) lane from a static workspace-name string. */
|
|
140
|
+
readonly __nomosFromPayload: true;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Name the PAYLOAD FIELD whose value routes a dynamic attested read (see {@link FromPayloadRef}). */
|
|
144
|
+
export function fromPayload(field: string): FromPayloadRef {
|
|
145
|
+
if (typeof field !== "string" || field.length === 0) {
|
|
146
|
+
throw new Error(`fromPayload(): the payload field name must be a non-empty string`);
|
|
147
|
+
}
|
|
148
|
+
return { payloadField: field, __nomosFromPayload: true };
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Shape predicate — a {@link FromPayloadRef} vs a static workspace-name string. Library-internal. */
|
|
152
|
+
export function isFromPayloadRef(v: unknown): v is FromPayloadRef {
|
|
153
|
+
return (
|
|
154
|
+
v !== null &&
|
|
155
|
+
typeof v === "object" &&
|
|
156
|
+
(v as { __nomosFromPayload?: unknown }).__nomosFromPayload === true &&
|
|
157
|
+
typeof (v as { payloadField?: unknown }).payloadField === "string"
|
|
158
|
+
);
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** The §4.4 freshness + allow-list vocabulary of one declared trust POLICY. */
|
|
162
|
+
export interface TrustPolicyOptions {
|
|
163
|
+
/** The reader-side aggregate whose folded rows are the acceptable-source set (genesis + keyHashes
|
|
164
|
+
* per row). Enrolling a row is a GATED WRITE (owner authority); revoking = archiving the row. */
|
|
165
|
+
readonly allowList: AggregateHandle;
|
|
166
|
+
/** The allow-list field (a `t.string()`) holding a row's source GENESIS oid. Inferred when the
|
|
167
|
+
* aggregate has exactly one string field whose name contains "genesis"; else required. */
|
|
168
|
+
readonly genesisField?: string;
|
|
169
|
+
/** The allow-list field (a `t.set(t.string())`) holding a row's accepted attestor keyHashes.
|
|
170
|
+
* Inferred when the aggregate has exactly one set-of-string field; else required. */
|
|
171
|
+
readonly keysField?: string;
|
|
172
|
+
/** §4.4a — wall-clock freshness bound in ms (judged against the intent's frozen HLC). */
|
|
173
|
+
readonly maxAgeMs?: number;
|
|
174
|
+
/** §4.4b — the epoch anchor: `"birth"` (the reader's own birth-cert epoch) or an integer floor. */
|
|
175
|
+
readonly minSourceEpoch?: "birth" | number;
|
|
176
|
+
/** §4.4c — maintain + enforce the monotone `SourceFrontier` (no regression per source genesis). */
|
|
177
|
+
readonly monotone?: boolean;
|
|
178
|
+
/** §5.3 — what a folded `SourceDishonor` fact does at verify: `"refuse"` or `"record"`. */
|
|
179
|
+
readonly onDishonor?: "refuse" | "record";
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
/** One declared trust POLICY — an exported {@link trustPolicy} value, auto-discovered by shape.
|
|
183
|
+
* Lowered into the `nomosAttestedReadGate` era key's `policies` map. */
|
|
184
|
+
export interface TrustPolicyDecl {
|
|
185
|
+
readonly policyId: string;
|
|
186
|
+
/** The resolved allow-list aggregate TYPE id. */
|
|
187
|
+
readonly allowListType: string;
|
|
188
|
+
/** The resolved genesis field LABEL. */
|
|
189
|
+
readonly genesisField: string;
|
|
190
|
+
/** The resolved keyHashes field LABEL. */
|
|
191
|
+
readonly keysField: string;
|
|
192
|
+
readonly maxAgeMs?: number;
|
|
193
|
+
readonly minSourceEpoch?: "birth" | number;
|
|
194
|
+
readonly monotone?: boolean;
|
|
195
|
+
readonly onDishonor?: "refuse" | "record";
|
|
196
|
+
readonly __nomosTrustPolicy: true;
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// The runtime policy registry — the dynamic read lane resolves a policy's monotone posture through
|
|
200
|
+
// the directive's readsFrom (which registers the queryId); this map lets the compile discovery + the
|
|
201
|
+
// validators resolve a decl by id. Keyed by policyId; later declaration wins.
|
|
202
|
+
const policyRegistry = new Map<string, TrustPolicyDecl>();
|
|
203
|
+
|
|
204
|
+
/** The registered trust policy for an id (`undefined` = undeclared). Library-internal. */
|
|
205
|
+
export function __policyFor(policyId: string): TrustPolicyDecl | undefined {
|
|
206
|
+
return policyRegistry.get(policyId);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* DECLARE a dynamic trust POLICY (dynamic_source_attested_reads.md): the acceptable-source set is
|
|
211
|
+
* LAW-STATE — the folded rows of `allowList`, each a `{ <genesisField>: string, <keysField>:
|
|
212
|
+
* set-of-string }`. A directive routes a runtime-named source through this policy with
|
|
213
|
+
* `.readsFrom(fromPayload("wsField"), theQuery, { policy: thisDecl })`. Auto-discovered by shape and
|
|
214
|
+
* lowered into the `nomosAttestedReadGate` era key's `policies` map. The field names are inferred by
|
|
215
|
+
* shape (see {@link TrustPolicyOptions}) or given explicitly; a bad allow-list is a NAMED compile error.
|
|
216
|
+
*/
|
|
217
|
+
export function trustPolicy(policyId: string, opts: TrustPolicyOptions): TrustPolicyDecl {
|
|
218
|
+
const al = opts.allowList as AggregateHandle | undefined;
|
|
219
|
+
if (
|
|
220
|
+
al === null ||
|
|
221
|
+
typeof al !== "object" ||
|
|
222
|
+
(al as { __isAggregateHandle?: unknown }).__isAggregateHandle !== true
|
|
223
|
+
) {
|
|
224
|
+
throw new Error(
|
|
225
|
+
`trustPolicy('${policyId}'): 'allowList' must be an aggregate handle — pass the aggregate(...) ` +
|
|
226
|
+
`whose rows hold the trusted sources ({ <genesis> string, <keyHashes> set-of-string })`,
|
|
227
|
+
);
|
|
228
|
+
}
|
|
229
|
+
const fields = al.fields as Record<string, Field>;
|
|
230
|
+
const stringFields = Object.keys(fields).filter((n) => (fields[n] as Field).kind === "string");
|
|
231
|
+
const setFields = Object.keys(fields).filter((n) => (fields[n] as Field).kind === "set");
|
|
232
|
+
|
|
233
|
+
let genesisField: string;
|
|
234
|
+
if (opts.genesisField !== undefined) {
|
|
235
|
+
if (!(opts.genesisField in fields)) {
|
|
236
|
+
throw new Error(
|
|
237
|
+
`trustPolicy('${policyId}'): genesisField '${opts.genesisField}' is not a field of aggregate '${al.id}'`,
|
|
238
|
+
);
|
|
239
|
+
}
|
|
240
|
+
if ((fields[opts.genesisField] as Field).kind !== "string") {
|
|
241
|
+
throw new Error(
|
|
242
|
+
`trustPolicy('${policyId}'): genesisField '${opts.genesisField}' must be a t.string() field (the source genesis oid)`,
|
|
243
|
+
);
|
|
244
|
+
}
|
|
245
|
+
genesisField = opts.genesisField;
|
|
246
|
+
} else {
|
|
247
|
+
const cands = stringFields.filter((n) => n.toLowerCase().includes("genesis"));
|
|
248
|
+
if (cands.length !== 1) {
|
|
249
|
+
throw new Error(
|
|
250
|
+
`trustPolicy('${policyId}'): cannot infer genesisField on aggregate '${al.id}' — declare exactly ` +
|
|
251
|
+
`ONE t.string() field whose name contains "genesis", or pass genesisField: "<name>" explicitly`,
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
genesisField = cands[0]!;
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
let keysField: string;
|
|
258
|
+
if (opts.keysField !== undefined) {
|
|
259
|
+
if (!(opts.keysField in fields)) {
|
|
260
|
+
throw new Error(
|
|
261
|
+
`trustPolicy('${policyId}'): keysField '${opts.keysField}' is not a field of aggregate '${al.id}'`,
|
|
262
|
+
);
|
|
263
|
+
}
|
|
264
|
+
if ((fields[opts.keysField] as Field).kind !== "set") {
|
|
265
|
+
throw new Error(
|
|
266
|
+
`trustPolicy('${policyId}'): keysField '${opts.keysField}' must be a t.set(t.string()) field (the accepted attestor keyHashes)`,
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
keysField = opts.keysField;
|
|
270
|
+
} else {
|
|
271
|
+
if (setFields.length !== 1) {
|
|
272
|
+
throw new Error(
|
|
273
|
+
`trustPolicy('${policyId}'): cannot infer keysField on aggregate '${al.id}' — declare exactly ` +
|
|
274
|
+
`ONE t.set(t.string()) field (the accepted attestor keyHashes), or pass keysField: "<name>" explicitly`,
|
|
275
|
+
);
|
|
276
|
+
}
|
|
277
|
+
keysField = setFields[0]!;
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
const decl: TrustPolicyDecl = {
|
|
281
|
+
policyId,
|
|
282
|
+
allowListType: al.id,
|
|
283
|
+
genesisField,
|
|
284
|
+
keysField,
|
|
285
|
+
...(opts.maxAgeMs !== undefined ? { maxAgeMs: opts.maxAgeMs } : {}),
|
|
286
|
+
...(opts.minSourceEpoch !== undefined ? { minSourceEpoch: opts.minSourceEpoch } : {}),
|
|
287
|
+
...(opts.monotone !== undefined ? { monotone: opts.monotone } : {}),
|
|
288
|
+
...(opts.onDishonor !== undefined ? { onDishonor: opts.onDishonor } : {}),
|
|
289
|
+
__nomosTrustPolicy: true,
|
|
290
|
+
};
|
|
291
|
+
policyRegistry.set(policyId, decl);
|
|
292
|
+
return decl;
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/** Shape predicate for compile discovery (the `isTrustSourceDecl` pattern). */
|
|
296
|
+
export function isTrustPolicyDecl(v: unknown): v is TrustPolicyDecl {
|
|
297
|
+
return (
|
|
298
|
+
v !== null &&
|
|
299
|
+
typeof v === "object" &&
|
|
300
|
+
(v as { __nomosTrustPolicy?: unknown }).__nomosTrustPolicy === true &&
|
|
301
|
+
typeof (v as { policyId?: unknown }).policyId === "string"
|
|
302
|
+
);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
/** One declared foreign-read edge on a directive — a STATIC workspace edge or a dynamic policy edge.
|
|
306
|
+
* The static edge emits `{queryId, from}`; the policy edge emits `{queryId, policy}` (the routing
|
|
307
|
+
* `fromPayload` field is DSL-internal — the gate judges by genesis, the name is transport). */
|
|
308
|
+
export type ForeignReadEdge =
|
|
309
|
+
| { readonly queryId: string; readonly from: string }
|
|
310
|
+
| { readonly queryId: string; readonly policy: string; readonly fromPayload: string };
|
|
311
|
+
|
|
312
|
+
// A dynamic (policy-routed) read carries no declared `from` string, so the read lane cannot resolve
|
|
313
|
+
// its monotone posture through `__trustFor(name)`. The directive's readsFrom registers the queryId of
|
|
314
|
+
// every MONOTONE policy edge here; the foreign read lane consults it to record the SourceFrontier.
|
|
315
|
+
const monotoneForeignQueries = new Set<string>();
|
|
316
|
+
/** Register a foreign query id whose governing policy is monotone. Library-internal. */
|
|
317
|
+
export function __registerMonotoneForeignQuery(queryId: string): void {
|
|
318
|
+
monotoneForeignQueries.add(queryId);
|
|
319
|
+
}
|
|
320
|
+
/** Is a foreign query governed by a monotone policy edge? Library-internal. */
|
|
321
|
+
export function __isMonotoneForeignQuery(queryId: string): boolean {
|
|
322
|
+
return monotoneForeignQueries.has(queryId);
|
|
323
|
+
}
|
|
324
|
+
|
|
128
325
|
// ── the SourceFrontier (§4.4c) — the reader-side monotone record of the source's position ──────
|
|
129
326
|
|
|
130
327
|
/** The deterministic instance-id prefix — `source-frontier:<source_genesis>` (keyed by the source's
|
package/src/build_package.ts
CHANGED
|
@@ -61,7 +61,13 @@ import {
|
|
|
61
61
|
} from "./birth_meter.js";
|
|
62
62
|
import { withBirthMeterExports } from "./birth_meter_ensure.js";
|
|
63
63
|
import { withClearDirectives } from "./clear_directive.js";
|
|
64
|
-
import {
|
|
64
|
+
import {
|
|
65
|
+
withAttestedReadExports,
|
|
66
|
+
isTrustSourceDecl,
|
|
67
|
+
isTrustPolicyDecl,
|
|
68
|
+
type TrustSourceDecl,
|
|
69
|
+
type TrustPolicyDecl,
|
|
70
|
+
} from "./attested.js";
|
|
65
71
|
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
66
72
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
67
73
|
import { collectValueObjectDescriptors } from "./value_object.js";
|
|
@@ -336,6 +342,15 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
336
342
|
const trustSources = [...trustByWs.values()].sort((a, b) =>
|
|
337
343
|
a.workspace < b.workspace ? -1 : a.workspace > b.workspace ? 1 : 0,
|
|
338
344
|
);
|
|
345
|
+
// Dynamic-source trust POLICIES (dynamic_source_attested_reads.md) — discovered by shape like
|
|
346
|
+
// trustSource. Later-wins by policy id; sorted for byte-determinism.
|
|
347
|
+
const policyById = new Map<string, TrustPolicyDecl>();
|
|
348
|
+
for (const v of Object.values(merged)) {
|
|
349
|
+
if (isTrustPolicyDecl(v)) policyById.set(v.policyId, v);
|
|
350
|
+
}
|
|
351
|
+
const trustPolicies = [...policyById.values()].sort((a, b) =>
|
|
352
|
+
a.policyId < b.policyId ? -1 : a.policyId > b.policyId ? 1 : 0,
|
|
353
|
+
);
|
|
339
354
|
const pabMarker =
|
|
340
355
|
parentAttestedBirthMarker(merged) ??
|
|
341
356
|
spec.modules.map((m) => parentAttestedBirthMarker(m)).find((x) => x !== null) ??
|
|
@@ -385,6 +400,7 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
385
400
|
// Omit-when-empty — the nomosAttestedReadGate era rule: a domain with no declared trust
|
|
386
401
|
// edge is byte-identical in the canonical manifest/USD.
|
|
387
402
|
...(trustSources.length > 0 ? { trustSources } : {}),
|
|
403
|
+
...(trustPolicies.length > 0 ? { trustPolicies } : {}),
|
|
388
404
|
...(dartImports.length > 0 ? { dartImports } : {}),
|
|
389
405
|
...(spec.dartRefImports !== undefined && spec.dartRefImports.length > 0
|
|
390
406
|
? { dartRefImports: [...spec.dartRefImports] }
|
package/src/codegen_dart.ts
CHANGED
|
@@ -3488,6 +3488,13 @@ export interface DomainModule {
|
|
|
3488
3488
|
* ADDITIVE + OPTIONAL: a domain with no trust edge omits this and is byte-identical (era rule).
|
|
3489
3489
|
*/
|
|
3490
3490
|
trustSources?: import("./attested.js").TrustSourceDecl[];
|
|
3491
|
+
/**
|
|
3492
|
+
* The domain's DYNAMIC-SOURCE TRUST POLICIES (dynamic_source_attested_reads.md): the exported
|
|
3493
|
+
* `trustPolicy(id, { allowList, … })` declarations, discovered by shape. Lowered — together with
|
|
3494
|
+
* directives' `.readsFrom(fromPayload(…), query, { policy })` edges — into the `nomosAttestedReadGate`
|
|
3495
|
+
* era key's `policies` map. ADDITIVE + OPTIONAL: a policy-free domain omits this (era rule).
|
|
3496
|
+
*/
|
|
3497
|
+
trustPolicies?: import("./attested.js").TrustPolicyDecl[];
|
|
3491
3498
|
/**
|
|
3492
3499
|
* The domain's NATIVE ZANZIBAR RELATION SCHEMA (`relation_schema.ts` — the userset-rewrite law,
|
|
3493
3500
|
* P1). Pure-data per-type relation rewrites lowered to the `nomosRelations` USD era key (NO JS on
|
package/src/codegen_proof.ts
CHANGED
|
@@ -1884,7 +1884,20 @@ export interface ProofLegs {
|
|
|
1884
1884
|
function isStandaloneCreate(d: Directive<unknown> | undefined): boolean {
|
|
1885
1885
|
return d?.marker === "creates" && typeof d.payloadSchema === "object"
|
|
1886
1886
|
&& d.birthsChild !== true && d.attestationAuthorizedGate === undefined
|
|
1887
|
-
&& d.requiresRelationGate === undefined
|
|
1887
|
+
&& d.requiresRelationGate === undefined
|
|
1888
|
+
// NOT a foreign-read directive (`.readsFrom(...)` — a static attested edge or a dynamic
|
|
1889
|
+
// policy edge): its plan reads a SIGNED cross-workspace attestation the single-workspace
|
|
1890
|
+
// offline proof plane cannot mint (that needs a second SOURCE workspace + its attestor key).
|
|
1891
|
+
// Dispatched offline it would throw at the un-served read; proven instead by the two-workspace
|
|
1892
|
+
// smoke (bench/scale/{attested_reads.test,dynamic_attested_reads.smoke}.mjs). Named below.
|
|
1893
|
+
&& (((d as { declaredForeignReads?: readonly unknown[] }).declaredForeignReads?.length ?? 0) === 0);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
/** Does this module carry ≥1 foreign-read directive (a static or dynamic-policy `.readsFrom`)? */
|
|
1897
|
+
function hasForeignReadDirective(mod: DomainModule): boolean {
|
|
1898
|
+
return (mod.directives as Directive<unknown>[]).some(
|
|
1899
|
+
(d) => ((d as { declaredForeignReads?: readonly unknown[] }).declaredForeignReads?.length ?? 0) > 0,
|
|
1900
|
+
);
|
|
1888
1901
|
}
|
|
1889
1902
|
|
|
1890
1903
|
/** Does `v` carry an RFC 7946 geometry object at `segs` (walked into the composite)? */
|
|
@@ -2772,6 +2785,23 @@ export function generateTsProof(modules: readonly DomainModule[], opts: TsProofO
|
|
|
2772
2785
|
);
|
|
2773
2786
|
}
|
|
2774
2787
|
|
|
2788
|
+
// THE ATTESTED / DYNAMIC-POLICY FOREIGN-READ LEG — named, honestly SKIPPED here. A `.readsFrom(...)`
|
|
2789
|
+
// directive's plan reads a SIGNED cross-workspace attestation; proving it needs a SECOND source
|
|
2790
|
+
// workspace, its read-attestor key, and a served+signed envelope — none of which the single-workspace,
|
|
2791
|
+
// client-driven offline proof plane can mint (the client has no `attestedRead` serving op). The full
|
|
2792
|
+
// offline two-workspace proof (birth a scratch source → seed a row → enrol its genesis → attested read
|
|
2793
|
+
// through the policy edge → admitted, plus the wrong-genesis/unenrolled negative + revocation +
|
|
2794
|
+
// verify_chain) lives in the bench harness against the real wasm.
|
|
2795
|
+
if (hasForeignReadDirective(mod)) {
|
|
2796
|
+
out.push(
|
|
2797
|
+
`// ${n()}. attested foreign-read leg: a .readsFrom(...) directive reads a SIGNED cross-workspace`,
|
|
2798
|
+
`// attestation — proving it needs a second SOURCE workspace + its attestor key, which this`,
|
|
2799
|
+
`// single-workspace client proof cannot mint. Proven in the two-workspace bench smoke.`,
|
|
2800
|
+
`console.log("– attested foreign-read leg SKIPPED — a .readsFrom(...) attested/policy read is proven in the two-workspace bench harness (bench/scale/{attested_reads.test,dynamic_attested_reads.smoke}.mjs), not this single-workspace offline proof");`,
|
|
2801
|
+
``,
|
|
2802
|
+
);
|
|
2803
|
+
}
|
|
2804
|
+
|
|
2775
2805
|
// THE AUTHORITY-CONFORMANCE LEG (the one-encoder + gate proof): runs on the LOCAL holon (fetch re-trapped),
|
|
2776
2806
|
// proving the generated client threads security.actor through THE ONE ENCODER and that an actorless gated
|
|
2777
2807
|
// offer is impossible to GENERATE (throws MissingOfferAuthority before any submit). No-op when un-gated.
|
package/src/directive.ts
CHANGED
|
@@ -24,6 +24,14 @@ import type { CertifiedReadDecl } from "./certified_read.js";
|
|
|
24
24
|
import type { RelationDecl } from "./relation.js";
|
|
25
25
|
import type { QueryDecl } from "./query.js";
|
|
26
26
|
import { BIRTH_METER_QUERIES } from "./birth_meter.js";
|
|
27
|
+
import {
|
|
28
|
+
isFromPayloadRef,
|
|
29
|
+
isTrustPolicyDecl,
|
|
30
|
+
__registerMonotoneForeignQuery,
|
|
31
|
+
type FromPayloadRef,
|
|
32
|
+
type TrustPolicyDecl,
|
|
33
|
+
type ForeignReadEdge,
|
|
34
|
+
} from "./attested_core.js";
|
|
27
35
|
|
|
28
36
|
export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
|
|
29
37
|
|
|
@@ -183,7 +191,7 @@ export interface Directive<P = unknown> {
|
|
|
183
191
|
* era key. OPTIONAL + ADDITIVE; OMITTED when empty (a foreign-read-free directive is
|
|
184
192
|
* byte-identical — the era rule).
|
|
185
193
|
*/
|
|
186
|
-
readonly declaredForeignReads?:
|
|
194
|
+
readonly declaredForeignReads?: ForeignReadEdge[];
|
|
187
195
|
/**
|
|
188
196
|
* The directive's DECLARED emit boundary: the event types its `plan` may EMIT,
|
|
189
197
|
* each with an optional `max` bound. Part of the domain identity. Defaults to `{}`
|
|
@@ -331,6 +339,18 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
331
339
|
* times; deduped by `(queryId, from)`. Purely additive (the era rule).
|
|
332
340
|
*/
|
|
333
341
|
readsFrom(sourceWorkspace: string, query: { id: string }): RequirableDirective<P>;
|
|
342
|
+
/**
|
|
343
|
+
* Declare ONE DYNAMIC (payload-routed) foreign read (dynamic_source_attested_reads.md): the plan may
|
|
344
|
+
* `read(theQuery, args, { from: payload.<field> })` — the routed workspace is chosen at runtime by the
|
|
345
|
+
* named payload field, while the trust judgment (genesis + accepted attestor keys) is a fold of the
|
|
346
|
+
* reader's own {@link TrustPolicyDecl} allow-list. Lowered into the era key as a `{queryId, policy}`
|
|
347
|
+
* edge (the routing field is DSL-internal — the gate judges by genesis, the name is transport).
|
|
348
|
+
*/
|
|
349
|
+
readsFrom(
|
|
350
|
+
source: FromPayloadRef,
|
|
351
|
+
query: { id: string },
|
|
352
|
+
opts: { policy: TrustPolicyDecl },
|
|
353
|
+
): RequirableDirective<P>;
|
|
334
354
|
/**
|
|
335
355
|
* Declare ONE entry of the directive's EMIT boundary: an `eventType` its `plan` may
|
|
336
356
|
* emit, with an optional `max` count bound. Callable MULTIPLE times to accumulate
|
|
@@ -671,17 +691,41 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
|
671
691
|
const mergedQueries = [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
672
692
|
return makeRequirable({ ...base, declaredReads: merged, declaredQueryReads: mergedQueries });
|
|
673
693
|
},
|
|
674
|
-
readsFrom(
|
|
675
|
-
|
|
676
|
-
|
|
694
|
+
readsFrom(
|
|
695
|
+
source: string | FromPayloadRef,
|
|
696
|
+
query: { id: string },
|
|
697
|
+
opts?: { policy: TrustPolicyDecl },
|
|
698
|
+
): RequirableDirective<P> {
|
|
699
|
+
// ATTESTED CROSS-WORKSPACE READ (slice B + dynamic policies): accumulate a STATIC (queryId, from)
|
|
700
|
+
// edge OR a DYNAMIC (queryId, policy, fromPayload) edge, dedup by canonical key, sort (only the
|
|
701
|
+
// SET is the contract — byte-determinism for the era key).
|
|
702
|
+
let entry: ForeignReadEdge;
|
|
703
|
+
if (isFromPayloadRef(source)) {
|
|
704
|
+
const policy = opts?.policy;
|
|
705
|
+
if (policy === undefined) {
|
|
706
|
+
throw new Error(
|
|
707
|
+
`readsFrom(fromPayload('${source.payloadField}'), '${query.id}'): a payload-routed foreign ` +
|
|
708
|
+
`read REQUIRES a trust policy — pass { policy: <trustPolicy(...) decl> } (the acceptable ` +
|
|
709
|
+
`sources are LAW-STATE, so the runtime-named workspace is judged against a folded allow-list).`,
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
if (!isTrustPolicyDecl(policy)) {
|
|
713
|
+
throw new Error(
|
|
714
|
+
`readsFrom(fromPayload('${source.payloadField}'), '${query.id}'): { policy } must be a ` +
|
|
715
|
+
`trustPolicy(...) declaration.`,
|
|
716
|
+
);
|
|
717
|
+
}
|
|
718
|
+
entry = { queryId: query.id, policy: policy.policyId, fromPayload: source.payloadField };
|
|
719
|
+
if (policy.monotone === true) __registerMonotoneForeignQuery(query.id);
|
|
720
|
+
} else {
|
|
721
|
+
entry = { queryId: query.id, from: source };
|
|
722
|
+
}
|
|
723
|
+
const canon = (e: ForeignReadEdge): string =>
|
|
724
|
+
"from" in e ? `0${e.from}${e.queryId}` : `1${e.policy}${e.fromPayload}${e.queryId}`;
|
|
677
725
|
const prior = base.declaredForeignReads ?? [];
|
|
678
|
-
const
|
|
679
|
-
|
|
680
|
-
const
|
|
681
|
-
byKey.set(key(entry), entry);
|
|
682
|
-
const merged = [...byKey.values()].sort((a, b) =>
|
|
683
|
-
a.from !== b.from ? (a.from < b.from ? -1 : 1) : a.queryId < b.queryId ? -1 : a.queryId > b.queryId ? 1 : 0,
|
|
684
|
-
);
|
|
726
|
+
const byKey = new Map<string, ForeignReadEdge>(prior.map((e) => [canon(e), e]));
|
|
727
|
+
byKey.set(canon(entry), entry);
|
|
728
|
+
const merged = [...byKey.values()].sort((a, b) => (canon(a) < canon(b) ? -1 : canon(a) > canon(b) ? 1 : 0));
|
|
685
729
|
return makeRequirable({ ...base, declaredForeignReads: merged });
|
|
686
730
|
},
|
|
687
731
|
emits(eventType: string, opts?: { max?: number }): RequirableDirective<P> {
|
package/src/index.ts
CHANGED
|
@@ -148,12 +148,18 @@ export {
|
|
|
148
148
|
trustParent,
|
|
149
149
|
PARENT_TRUST_SENTINEL,
|
|
150
150
|
foreignQuery,
|
|
151
|
+
trustPolicy,
|
|
152
|
+
fromPayload,
|
|
151
153
|
NomosSourceFrontier,
|
|
152
154
|
sourceFrontierId,
|
|
153
155
|
SOURCE_FRONTIER_PREFIX,
|
|
154
156
|
nomosSourceFrontierEnsure,
|
|
155
157
|
type TrustSourceDecl,
|
|
156
158
|
type TrustSourceOptions,
|
|
159
|
+
type TrustPolicyDecl,
|
|
160
|
+
type TrustPolicyOptions,
|
|
161
|
+
type FromPayloadRef,
|
|
162
|
+
type ForeignReadEdge,
|
|
157
163
|
type ForeignQueryRef,
|
|
158
164
|
} from "./attested.js";
|
|
159
165
|
// ATTESTATION RECEIPTS + EQUIVOCATION ACCOUNTABILITY (slice B stage 3 — §5): the source-side
|
package/src/law_check.ts
CHANGED
|
@@ -44,6 +44,7 @@ import { finishSum } from "./sum.js";
|
|
|
44
44
|
import { finishExtremum } from "./extremum.js";
|
|
45
45
|
import { finishExists } from "./exists.js";
|
|
46
46
|
import { clearDirectiveId } from "./clear_directive.js";
|
|
47
|
+
import type { ForeignReadEdge } from "./attested_core.js";
|
|
47
48
|
|
|
48
49
|
/** One author-time finding — the gate refusal pre-fired at compile, or a courtesy note. */
|
|
49
50
|
export interface Finding {
|
|
@@ -994,6 +995,111 @@ function topLevelPortReadFindings(mod: DomainModule, portReadingDomains: Readonl
|
|
|
994
995
|
];
|
|
995
996
|
}
|
|
996
997
|
|
|
998
|
+
// ── ATTESTED-READ twins (dynamic_source_attested_reads.md §"proof + check") ──────────────────────
|
|
999
|
+
// (a) a foreign read whose (directive, query) has no declared trust edge → REFUSE naming the fix;
|
|
1000
|
+
// (b) a trustPolicy whose allow-list aggregate has no directive writing it → WARN (unpopulatable);
|
|
1001
|
+
// (c) a query declared BOTH as a local .reads() premise AND a foreign policy read → REFUSE (laundering).
|
|
1002
|
+
|
|
1003
|
+
/** The FOREIGN query ids a directive's plan actually reads (`read(q, …, { from })`) on the one probe
|
|
1004
|
+
* sample — `undefined` when the plan can't be probed (unsynthesizable payload / it threw). A clean re-run
|
|
1005
|
+
* under a read port that answers both lanes; every side-effect sink is drained (never leaks). */
|
|
1006
|
+
function foreignReadsPerformed(dir: Directive<unknown>, where: string): Set<string> | undefined {
|
|
1007
|
+
let synth: SynthesizedPayload;
|
|
1008
|
+
try {
|
|
1009
|
+
synth = synthesizePayload(dir.payloadSchema as z.ZodTypeAny, where);
|
|
1010
|
+
} catch {
|
|
1011
|
+
return undefined;
|
|
1012
|
+
}
|
|
1013
|
+
const payload: Record<string, unknown> = { ...synth.value };
|
|
1014
|
+
for (const f of synth.autoStamped) payload[f] = "2026-01-01T00:00:00Z";
|
|
1015
|
+
const performed = new Set<string>();
|
|
1016
|
+
let ok = true;
|
|
1017
|
+
withBirthProbeGlobals(() => {
|
|
1018
|
+
const g = globalThis as unknown as { nomos: Record<string, unknown> };
|
|
1019
|
+
const prevRead = g.nomos.read;
|
|
1020
|
+
const stubRows = [{ id: "probe", data: {} }];
|
|
1021
|
+
g.nomos.read = (_q: string, _a: unknown, o?: { from?: string }) =>
|
|
1022
|
+
o?.from
|
|
1023
|
+
? {
|
|
1024
|
+
rows: stubRows,
|
|
1025
|
+
attestation: { v: 1, source_genesis: "probe-genesis", source_head: "h", source_epoch: 0, attested_at_ms: 0 },
|
|
1026
|
+
}
|
|
1027
|
+
: stubRows;
|
|
1028
|
+
__resetAuthoring();
|
|
1029
|
+
__resetReads();
|
|
1030
|
+
try {
|
|
1031
|
+
dir.plan(payload as never, deterministicPorts({ physical: 1, replica: 1, principal: "principal:probe" }));
|
|
1032
|
+
for (const r of __drainReads()) {
|
|
1033
|
+
if ((r as { attestation?: unknown }).attestation !== undefined) performed.add(r.queryId);
|
|
1034
|
+
}
|
|
1035
|
+
} catch {
|
|
1036
|
+
ok = false;
|
|
1037
|
+
} finally {
|
|
1038
|
+
try { __drainAuthoring(); } catch { /* drained */ }
|
|
1039
|
+
try { __drainReads(); } catch { /* drained */ }
|
|
1040
|
+
g.nomos.read = prevRead;
|
|
1041
|
+
}
|
|
1042
|
+
});
|
|
1043
|
+
return ok ? performed : undefined;
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
function attestedReadFindings(mod: DomainModule): Finding[] {
|
|
1047
|
+
const domain = mod.domain ?? mod.name;
|
|
1048
|
+
const findings: Finding[] = [];
|
|
1049
|
+
const directives = mod.directives as Directive<unknown>[];
|
|
1050
|
+
|
|
1051
|
+
// (b) unpopulatable trust set — a policy allow-list no directive writes.
|
|
1052
|
+
const writtenTypes = new Set(directives.map((d) => (d as { aggregateId?: string }).aggregateId).filter(Boolean) as string[]);
|
|
1053
|
+
for (const pol of mod.trustPolicies ?? []) {
|
|
1054
|
+
if (!writtenTypes.has(pol.allowListType)) {
|
|
1055
|
+
findings.push({
|
|
1056
|
+
domain,
|
|
1057
|
+
level: "warn",
|
|
1058
|
+
what: `trustPolicy '${pol.policyId}' trusts sources enrolled in aggregate '${pol.allowListType}', but NO directive in this domain writes that aggregate — the trust set is unpopulatable (no gated enrolment lane), so every dynamic read under this policy will refuse (untrusted source)`,
|
|
1059
|
+
fix: `add a gated enrolment directive, e.g. directive("enroll…").creates(${pol.allowListType}).requires("<ownerCap>") whose payload carries { <genesis> string, <keyHashes> string[] }`,
|
|
1060
|
+
});
|
|
1061
|
+
}
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
for (const dir of directives) {
|
|
1065
|
+
const edges = (dir as { declaredForeignReads?: ForeignReadEdge[] }).declaredForeignReads ?? [];
|
|
1066
|
+
const declaredQueryIds = new Set(edges.map((e) => e.queryId));
|
|
1067
|
+
const localQueryIds = new Set(
|
|
1068
|
+
((dir as { declaredQueryReads?: { id: string }[] }).declaredQueryReads ?? []).map((q) => q.id),
|
|
1069
|
+
);
|
|
1070
|
+
|
|
1071
|
+
// (c) laundering — a query cannot be both a local .reads() premise AND a foreign read.
|
|
1072
|
+
for (const qid of declaredQueryIds) {
|
|
1073
|
+
if (localQueryIds.has(qid)) {
|
|
1074
|
+
findings.push({
|
|
1075
|
+
domain,
|
|
1076
|
+
directiveId: dir.id,
|
|
1077
|
+
level: "refuse",
|
|
1078
|
+
what: `query '${qid}' is declared BOTH as a local .reads() premise AND a foreign read on directive '${dir.id}' — a query cannot be both local (same-workspace CAS) and foreign (a signed cross-workspace attestation)`,
|
|
1079
|
+
fix: `drop one: keep .reads(${qid}) for a same-workspace captured read, OR keep the foreign .readsFrom(...) edge and remove the local .reads(${qid})`,
|
|
1080
|
+
});
|
|
1081
|
+
}
|
|
1082
|
+
}
|
|
1083
|
+
|
|
1084
|
+
// (a) an undeclared foreign read — the plan routes read(q, …, { from }) with no trust edge for q.
|
|
1085
|
+
const performed = foreignReadsPerformed(dir, `${domain}/${dir.id}`);
|
|
1086
|
+
if (performed !== undefined) {
|
|
1087
|
+
for (const qid of performed) {
|
|
1088
|
+
if (!declaredQueryIds.has(qid)) {
|
|
1089
|
+
findings.push({
|
|
1090
|
+
domain,
|
|
1091
|
+
directiveId: dir.id,
|
|
1092
|
+
level: "refuse",
|
|
1093
|
+
what: `directive '${dir.id}' reads a foreign workspace — read('${qid}', …, { from }) — but declares no trust edge for '${qid}', so the gate refuses it on every lane`,
|
|
1094
|
+
fix: `declare the edge: .readsFrom(fromPayload("<wsField>"), <${qid} query>, { policy: <trustPolicy(...) decl> }) for a payload-routed source, or .readsFrom("<ws>", <query>) + trustSource("<ws>", …) for a fixed one`,
|
|
1095
|
+
});
|
|
1096
|
+
}
|
|
1097
|
+
}
|
|
1098
|
+
}
|
|
1099
|
+
}
|
|
1100
|
+
return findings;
|
|
1101
|
+
}
|
|
1102
|
+
|
|
997
1103
|
/**
|
|
998
1104
|
* Run every author-time twin over the composed domain modules and return the findings, ordered
|
|
999
1105
|
* refuse → warn → note (stable within a level, by domain/directive appearance). This is the ONE
|
|
@@ -1044,6 +1150,7 @@ export function checkDomainModules(
|
|
|
1044
1150
|
findings.push(...dottedQueryKeyFindings(mod, aggById));
|
|
1045
1151
|
findings.push(...nullBanFindings(mod, aggById));
|
|
1046
1152
|
findings.push(...nullClearAuthorityFindings(mod, aggById));
|
|
1153
|
+
findings.push(...attestedReadFindings(mod));
|
|
1047
1154
|
}
|
|
1048
1155
|
|
|
1049
1156
|
// The BIRTH-RECIPE twin (unify): fold the scattered `.births()` recipe diagnostics into the
|
package/src/read.ts
CHANGED
|
@@ -19,7 +19,7 @@
|
|
|
19
19
|
*/
|
|
20
20
|
import type { QueryDecl } from "./query.js";
|
|
21
21
|
import type { WireRead, WireReadAttestation } from "./wire.js";
|
|
22
|
-
import { NomosSourceFrontier, sourceFrontierId, __trustFor, __ensure } from "./attested_core.js";
|
|
22
|
+
import { NomosSourceFrontier, sourceFrontierId, __trustFor, __isMonotoneForeignQuery, __ensure } from "./attested_core.js";
|
|
23
23
|
|
|
24
24
|
// ── the per-dispatch read footprint — library state drained by `executeDirectiveToIntent` (mirrors the authoring sink) ──
|
|
25
25
|
let footprint: WireRead[] = [];
|
|
@@ -101,7 +101,10 @@ export function read<T = unknown>(
|
|
|
101
101
|
);
|
|
102
102
|
}
|
|
103
103
|
footprint.push({ queryId: query.id, args, result: answer.rows, attestation: att });
|
|
104
|
-
|
|
104
|
+
// `from` is a STATIC declared workspace for a trustSource edge, or a RUNTIME payload value for a
|
|
105
|
+
// dynamic policy edge — so the monotone posture comes either from the static edge (`__trustFor`) or
|
|
106
|
+
// from the policy the directive registered for this query id. Either way the frontier is genesis-keyed.
|
|
107
|
+
if (__trustFor(from)?.monotone === true || __isMonotoneForeignQuery(query.id)) {
|
|
105
108
|
const frontier = __ensure(NomosSourceFrontier, sourceFrontierId(att.source_genesis)) as {
|
|
106
109
|
set(f: string, v: unknown): unknown;
|
|
107
110
|
};
|
package/src/shape_compat.ts
CHANGED
|
@@ -54,10 +54,13 @@ export interface DomainInterface {
|
|
|
54
54
|
readonly directives: Record<string, DirectivePayloadShape>;
|
|
55
55
|
/** keyed by AGGREGATE SID. */
|
|
56
56
|
readonly aggregates: Record<string, AggregateReadShape>;
|
|
57
|
-
/** ATTESTED READS (slice B): directive id → its declared
|
|
58
|
-
*
|
|
59
|
-
* law
|
|
60
|
-
readonly foreignReads?: Record<
|
|
57
|
+
/** ATTESTED READS (slice B + dynamic policies): directive id → its declared foreign-read edges —
|
|
58
|
+
* a STATIC `{queryId, from}` edge or a dynamic `{queryId, policy, fromPayload}` edge — the
|
|
59
|
+
* cross-workspace dependencies this law consumes on its write path. OMITTED when none (era rule). */
|
|
60
|
+
readonly foreignReads?: Record<
|
|
61
|
+
string,
|
|
62
|
+
{ queryId: string; from?: string; policy?: string; fromPayload?: string }[]
|
|
63
|
+
>;
|
|
61
64
|
}
|
|
62
65
|
|
|
63
66
|
/** The verdict: compatible iff nothing is missing and nothing is type-incompatible.
|
|
@@ -161,8 +164,9 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
|
|
|
161
164
|
Object.assign(aggregates, sortedRec(aggEntries));
|
|
162
165
|
// ATTESTED READS (slice B): the declared trust edges — so shape-compat + client codegen can
|
|
163
166
|
// SEE the cross-workspace dependency (typed clients don't call it; it's write-path-internal).
|
|
164
|
-
|
|
165
|
-
|
|
167
|
+
type ForeignEdge = { queryId: string; from?: string; policy?: string; fromPayload?: string };
|
|
168
|
+
const foreignReadEntries: Array<[string, ForeignEdge[]]> = [];
|
|
169
|
+
for (const d of (mod.directives ?? []) as Array<{ id?: unknown; declaredForeignReads?: ForeignEdge[] }>) {
|
|
166
170
|
if (!d || typeof d.id !== "string") continue;
|
|
167
171
|
if (d.declaredForeignReads !== undefined && d.declaredForeignReads.length > 0) {
|
|
168
172
|
foreignReadEntries.push([d.id, d.declaredForeignReads.map((e) => ({ ...e }))]);
|
package/src/usd.ts
CHANGED
|
@@ -44,6 +44,7 @@ import type { DomainModule } from "./codegen_dart.js";
|
|
|
44
44
|
import { domainInterface } from "./shape_compat.js";
|
|
45
45
|
import { relationType, user, key, anyUser, qualifiedTypeId, qualifyRelationType } from "./relation_schema.js";
|
|
46
46
|
import { harvestPrincipalFlows } from "./wire_encode.js";
|
|
47
|
+
import { zodKind, zodObjectShape } from "./codegen_ts.js";
|
|
47
48
|
import { composeSharedRelationTypes } from "./relation_composition.js";
|
|
48
49
|
import { collectValueObjectUsages } from "./value_object.js";
|
|
49
50
|
import {
|
|
@@ -459,7 +460,29 @@ export interface UsdLayer {
|
|
|
459
460
|
onDishonor: "refuse" | "record";
|
|
460
461
|
}
|
|
461
462
|
>;
|
|
462
|
-
|
|
463
|
+
/**
|
|
464
|
+
* DYNAMIC-SOURCE trust POLICIES (dynamic_source_attested_reads.md): the acceptable-source SET is
|
|
465
|
+
* law-state — a reader-side allow-list aggregate. A directive edge may route through a policy
|
|
466
|
+
* (`{queryId, policy}`) instead of a fixed workspace; the gate resolves the envelope's genesis
|
|
467
|
+
* against the folded `allowList` rows (genesis field) → the row's `keysField` accepted keys.
|
|
468
|
+
* OMITTED when no policy is declared (byte-identical to a policy-free bundle — the era rule).
|
|
469
|
+
*/
|
|
470
|
+
readonly policies?: Record<
|
|
471
|
+
string,
|
|
472
|
+
{
|
|
473
|
+
allowList: string;
|
|
474
|
+
genesisField: string;
|
|
475
|
+
keysField: string;
|
|
476
|
+
maxAgeMs?: number;
|
|
477
|
+
minSourceEpoch?: "birth" | number;
|
|
478
|
+
monotone?: boolean;
|
|
479
|
+
onDishonor?: "refuse" | "record";
|
|
480
|
+
}
|
|
481
|
+
>;
|
|
482
|
+
readonly directives: Record<
|
|
483
|
+
string,
|
|
484
|
+
({ queryId: string; from: string } | { queryId: string; policy: string })[]
|
|
485
|
+
>;
|
|
463
486
|
};
|
|
464
487
|
/**
|
|
465
488
|
* THE READ-VISIBILITY GATE DECLARATION (the native-relations read gate).
|
|
@@ -566,6 +589,27 @@ export interface UsdDocument {
|
|
|
566
589
|
readonly layers: UsdLayer[];
|
|
567
590
|
}
|
|
568
591
|
|
|
592
|
+
/** The payload field kinds of a directive — `"string-required"` for a required `z.string()`, else the
|
|
593
|
+
* zod kind (`"optional"`, `"number"`, …). Used to validate a `fromPayload` routing field. Empty when
|
|
594
|
+
* the payload isn't a plain object (a shape the check can't reason about — the runtime gate arbitrates). */
|
|
595
|
+
function payloadFieldKinds(dir: { payloadSchema?: unknown } | undefined): Map<string, string> {
|
|
596
|
+
const m = new Map<string, string>();
|
|
597
|
+
const schema = dir?.payloadSchema as Parameters<typeof zodKind>[0] | undefined;
|
|
598
|
+
if (schema === undefined) return m;
|
|
599
|
+
let shape: Record<string, Parameters<typeof zodKind>[0]>;
|
|
600
|
+
try {
|
|
601
|
+
if (zodKind(schema) !== "object") return m;
|
|
602
|
+
shape = zodObjectShape(schema);
|
|
603
|
+
} catch {
|
|
604
|
+
return m;
|
|
605
|
+
}
|
|
606
|
+
for (const [name, f] of Object.entries(shape)) {
|
|
607
|
+
const k = zodKind(f);
|
|
608
|
+
m.set(name, k === "string" ? "string-required" : k);
|
|
609
|
+
}
|
|
610
|
+
return m;
|
|
611
|
+
}
|
|
612
|
+
|
|
569
613
|
/** Sort prims by their `path` (canonical structure). */
|
|
570
614
|
function sortPrims(prims: UsdPrim[]): UsdPrim[] {
|
|
571
615
|
return [...prims].sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
|
@@ -984,47 +1028,122 @@ export function emitUsd(
|
|
|
984
1028
|
// trustSource declarations. Emitted iff ≥1 directive declares a foreign read (the era
|
|
985
1029
|
// rule). A declared `from` with NO matching trustSource is a NAMED COMPILE ERROR — the
|
|
986
1030
|
// kernel would fail it closed anyway; refuse loudly at build instead.
|
|
987
|
-
|
|
1031
|
+
type ForeignEdgeInternal =
|
|
1032
|
+
| { queryId: string; from: string }
|
|
1033
|
+
| { queryId: string; policy: string; fromPayload: string };
|
|
1034
|
+
type ForeignEdgeEmit = { queryId: string; from: string } | { queryId: string; policy: string };
|
|
1035
|
+
const foreignDirectives: Record<string, ForeignEdgeInternal[]> = {};
|
|
988
1036
|
for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
989
|
-
const fr = (d as { declaredForeignReads?:
|
|
1037
|
+
const fr = (d as { declaredForeignReads?: ForeignEdgeInternal[] }).declaredForeignReads;
|
|
990
1038
|
if (fr !== undefined && fr.length > 0) foreignDirectives[d.id] = fr.map((e) => ({ ...e }));
|
|
991
1039
|
}
|
|
992
1040
|
let nomosAttestedReadGate: UsdLayer["nomosAttestedReadGate"];
|
|
993
1041
|
if (Object.keys(foreignDirectives).length > 0) {
|
|
994
1042
|
const trust = (l.module as { trustSources?: readonly import("./attested.js").TrustSourceDecl[] }).trustSources ?? [];
|
|
995
1043
|
const trustByWs = new Map(trust.map((tsrc) => [tsrc.workspace, tsrc]));
|
|
1044
|
+
const policies = (l.module as { trustPolicies?: readonly import("./attested.js").TrustPolicyDecl[] }).trustPolicies ?? [];
|
|
1045
|
+
const policyById = new Map(policies.map((p) => [p.policyId, p]));
|
|
1046
|
+
const aggIds = new Set(l.module.aggregates.map((a) => a.id));
|
|
1047
|
+
// A policy-referenced allow-list must be a real aggregate of THIS module (the fold happens in
|
|
1048
|
+
// the reader's own state) — else the gate could never resolve it (fail-closed at build).
|
|
1049
|
+
for (const p of policies) {
|
|
1050
|
+
if (!aggIds.has(p.allowListType)) {
|
|
1051
|
+
throw new Error(
|
|
1052
|
+
`trustPolicy('${p.policyId}'): its allowList aggregate '${p.allowListType}' is not an ` +
|
|
1053
|
+
`aggregate of this domain — the acceptable-source set is the reader's OWN folded state, ` +
|
|
1054
|
+
`so the allow-list must be declared (and written by a gated enrolment directive) here.`,
|
|
1055
|
+
);
|
|
1056
|
+
}
|
|
1057
|
+
}
|
|
996
1058
|
const sourcesRec: NonNullable<UsdLayer["nomosAttestedReadGate"]>["sources"] = {};
|
|
1059
|
+
const policiesRec: NonNullable<NonNullable<UsdLayer["nomosAttestedReadGate"]>["policies"]> = {};
|
|
1060
|
+
const emitDirectives: Record<string, ForeignEdgeEmit[]> = {};
|
|
997
1061
|
for (const [dirId, edges] of Object.entries(foreignDirectives)) {
|
|
1062
|
+
const dir = l.module.directives.find((d) => d.id === dirId);
|
|
1063
|
+
const payloadShape = payloadFieldKinds(dir);
|
|
1064
|
+
const emitted: ForeignEdgeEmit[] = [];
|
|
998
1065
|
for (const e of edges) {
|
|
999
|
-
|
|
1000
|
-
|
|
1001
|
-
|
|
1002
|
-
|
|
1003
|
-
`
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
sourcesRec[e.from]
|
|
1009
|
-
|
|
1010
|
-
|
|
1011
|
-
|
|
1012
|
-
|
|
1013
|
-
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1066
|
+
if ("from" in e) {
|
|
1067
|
+
const tsrc = trustByWs.get(e.from);
|
|
1068
|
+
if (tsrc === undefined) {
|
|
1069
|
+
throw new Error(
|
|
1070
|
+
`directive '${dirId}' declares .readsFrom('${e.from}', '${e.queryId}') but the ` +
|
|
1071
|
+
`module exports no trustSource('${e.from}', …) — the trust edge must be declared ` +
|
|
1072
|
+
`in law (attested_reads_design.md §3.1)`,
|
|
1073
|
+
);
|
|
1074
|
+
}
|
|
1075
|
+
if (sourcesRec[e.from] === undefined) {
|
|
1076
|
+
sourcesRec[e.from] = {
|
|
1077
|
+
genesis: tsrc.genesis,
|
|
1078
|
+
...(tsrc.keyHashes !== undefined && tsrc.keyHashes.length > 0
|
|
1079
|
+
? { keyHashes: [...tsrc.keyHashes].sort() }
|
|
1080
|
+
: {}),
|
|
1081
|
+
...(tsrc.maxAgeMs !== undefined ? { maxAgeMs: tsrc.maxAgeMs } : {}),
|
|
1082
|
+
...(tsrc.minSourceEpoch !== undefined ? { minSourceEpoch: tsrc.minSourceEpoch } : {}),
|
|
1083
|
+
...(tsrc.monotone === true ? { monotone: true } : {}),
|
|
1084
|
+
roster: tsrc.roster ?? "pinned",
|
|
1085
|
+
onDishonor: tsrc.onDishonor ?? "refuse",
|
|
1086
|
+
};
|
|
1087
|
+
}
|
|
1088
|
+
emitted.push({ queryId: e.queryId, from: e.from });
|
|
1089
|
+
} else {
|
|
1090
|
+
const pol = policyById.get(e.policy);
|
|
1091
|
+
if (pol === undefined) {
|
|
1092
|
+
throw new Error(
|
|
1093
|
+
`directive '${dirId}' declares .readsFrom(fromPayload('${e.fromPayload}'), '${e.queryId}', ` +
|
|
1094
|
+
`{ policy: '${e.policy}' }) but the module exports no trustPolicy('${e.policy}', …) — the ` +
|
|
1095
|
+
`policy must be declared in law (dynamic_source_attested_reads.md).`,
|
|
1096
|
+
);
|
|
1097
|
+
}
|
|
1098
|
+
// The routing field must be a REQUIRED string in the directive's payload (the workspace
|
|
1099
|
+
// name is chosen at runtime; a missing/optional/non-string field could never route).
|
|
1100
|
+
const kind = payloadShape.get(e.fromPayload);
|
|
1101
|
+
if (kind === undefined) {
|
|
1102
|
+
throw new Error(
|
|
1103
|
+
`directive '${dirId}': .readsFrom(fromPayload('${e.fromPayload}'), …) names a payload ` +
|
|
1104
|
+
`field that is not declared in the directive's payload — add a required z.string() ` +
|
|
1105
|
+
`'${e.fromPayload}' to the payload (it carries the routed source workspace name).`,
|
|
1106
|
+
);
|
|
1107
|
+
}
|
|
1108
|
+
if (kind !== "string-required") {
|
|
1109
|
+
throw new Error(
|
|
1110
|
+
`directive '${dirId}': the fromPayload routing field '${e.fromPayload}' must be a ` +
|
|
1111
|
+
`REQUIRED z.string() in the payload (it names the routed source workspace) — got '${kind}'.`,
|
|
1112
|
+
);
|
|
1113
|
+
}
|
|
1114
|
+
if (policiesRec[e.policy] === undefined) {
|
|
1115
|
+
policiesRec[e.policy] = {
|
|
1116
|
+
// Qualify the allow-list type by the owning domain iff it is namespaced — the
|
|
1117
|
+
// gate scans rows by their stored `__type`, which the engine stamps qualified
|
|
1118
|
+
// (`qaType`, the SAME read-projection namespacing rows/schema keys use). A bare
|
|
1119
|
+
// (unqualified) allowList would match no folded row → every dynamic read refuses.
|
|
1120
|
+
allowList: qaType(pol.allowListType),
|
|
1121
|
+
genesisField: pol.genesisField,
|
|
1122
|
+
keysField: pol.keysField,
|
|
1123
|
+
...(pol.maxAgeMs !== undefined ? { maxAgeMs: pol.maxAgeMs } : {}),
|
|
1124
|
+
...(pol.minSourceEpoch !== undefined ? { minSourceEpoch: pol.minSourceEpoch } : {}),
|
|
1125
|
+
...(pol.monotone === true ? { monotone: true } : {}),
|
|
1126
|
+
...(pol.onDishonor !== undefined ? { onDishonor: pol.onDishonor } : {}),
|
|
1127
|
+
};
|
|
1128
|
+
}
|
|
1129
|
+
emitted.push({ queryId: e.queryId, policy: e.policy });
|
|
1019
1130
|
}
|
|
1020
1131
|
}
|
|
1132
|
+
emitDirectives[dirId] = emitted;
|
|
1021
1133
|
}
|
|
1022
1134
|
// SORTED-KEY records (byte-determinism — matching the layered flatten).
|
|
1023
1135
|
const sortedSources: typeof sourcesRec = {};
|
|
1024
1136
|
for (const k of Object.keys(sourcesRec).sort()) sortedSources[k] = sourcesRec[k]!;
|
|
1025
|
-
const
|
|
1026
|
-
for (const k of Object.keys(
|
|
1027
|
-
|
|
1137
|
+
const sortedPolicies: typeof policiesRec = {};
|
|
1138
|
+
for (const k of Object.keys(policiesRec).sort()) sortedPolicies[k] = policiesRec[k]!;
|
|
1139
|
+
const sortedDirectives: Record<string, ForeignEdgeEmit[]> = {};
|
|
1140
|
+
for (const k of Object.keys(emitDirectives).sort()) sortedDirectives[k] = emitDirectives[k]!;
|
|
1141
|
+
nomosAttestedReadGate = {
|
|
1142
|
+
v: 1 as const,
|
|
1143
|
+
sources: sortedSources,
|
|
1144
|
+
...(Object.keys(sortedPolicies).length > 0 ? { policies: sortedPolicies } : {}),
|
|
1145
|
+
directives: sortedDirectives,
|
|
1146
|
+
};
|
|
1028
1147
|
}
|
|
1029
1148
|
// THE BIRTH GATE (G3 — birth is a declared kernel primitive): the SORTED set of directive ids
|
|
1030
1149
|
// declaring `.births()`. The kernel reads this off the manifest (never a domain aggregate) to know an
|
package/src/usd_layers.ts
CHANGED
|
@@ -835,20 +835,32 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
835
835
|
// layers, SORTED keys (matching the composed emission). Omit-when-empty (era rule).
|
|
836
836
|
let attestedReadGate: UsdLayer["nomosAttestedReadGate"];
|
|
837
837
|
{
|
|
838
|
-
type
|
|
838
|
+
type Gate = NonNullable<UsdLayer["nomosAttestedReadGate"]>;
|
|
839
|
+
type SrcSpec = Gate["sources"][string];
|
|
840
|
+
type PolSpec = NonNullable<Gate["policies"]>[string];
|
|
841
|
+
type EdgeList = Gate["directives"][string];
|
|
839
842
|
const srcs = new Map<string, SrcSpec>();
|
|
840
|
-
const
|
|
843
|
+
const pols = new Map<string, PolSpec>();
|
|
844
|
+
const dirs = new Map<string, EdgeList>();
|
|
841
845
|
for (const l of stack) {
|
|
842
846
|
if (l.nomosAttestedReadGate === undefined) continue;
|
|
843
847
|
for (const [ws, spec] of Object.entries(l.nomosAttestedReadGate.sources)) srcs.set(ws, spec);
|
|
848
|
+
for (const [id, spec] of Object.entries(l.nomosAttestedReadGate.policies ?? {})) pols.set(id, spec);
|
|
844
849
|
for (const [id, edges] of Object.entries(l.nomosAttestedReadGate.directives)) dirs.set(id, edges);
|
|
845
850
|
}
|
|
846
851
|
if (dirs.size > 0) {
|
|
847
852
|
const sourcesRec: Record<string, SrcSpec> = {};
|
|
848
853
|
for (const k of [...srcs.keys()].sort()) sourcesRec[k] = srcs.get(k)!;
|
|
849
|
-
const
|
|
854
|
+
const policiesRec: Record<string, PolSpec> = {};
|
|
855
|
+
for (const k of [...pols.keys()].sort()) policiesRec[k] = pols.get(k)!;
|
|
856
|
+
const directivesRec: Record<string, EdgeList> = {};
|
|
850
857
|
for (const k of [...dirs.keys()].sort()) directivesRec[k] = dirs.get(k)!;
|
|
851
|
-
attestedReadGate = {
|
|
858
|
+
attestedReadGate = {
|
|
859
|
+
v: 1,
|
|
860
|
+
sources: sourcesRec,
|
|
861
|
+
...(Object.keys(policiesRec).length > 0 ? { policies: policiesRec } : {}),
|
|
862
|
+
directives: directivesRec,
|
|
863
|
+
};
|
|
852
864
|
}
|
|
853
865
|
}
|
|
854
866
|
// THE BIRTH GATE (G3): the SORTED union of every layer's `.births()` directive ids. Omit-when-empty
|