@githolon/dsl 0.34.1 → 0.35.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/attested.ts +74 -0
- package/src/attested_core.ts +158 -0
- package/src/authoring.ts +5 -0
- package/src/build_package.ts +16 -0
- package/src/codegen_dart.ts +8 -0
- package/src/compile_package_main.ts +6 -1
- package/src/directive.ts +31 -0
- package/src/engine_entry.ts +2 -1
- package/src/index.ts +13 -0
- package/src/read.ts +44 -5
- package/src/read_projection_manifest.ts +14 -1
- package/src/shape_compat.ts +15 -0
- package/src/usd.ts +75 -0
- package/src/usd_layers.ts +32 -0
- package/src/wire.ts +36 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.35.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
ADDED
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ATTESTED CROSS-WORKSPACE CAPTURED READS (slice B — `architecture/attested_reads_design.md`).
|
|
8
|
+
*
|
|
9
|
+
* The doctrine: *"trusting my parent not to lie, I enforce what I admit by checking facts from
|
|
10
|
+
* workspaces I choose to trust, via a captured and attested read."* The trust boundary is DECLARED
|
|
11
|
+
* IN LAW ({@link trustSource} → the `nomosAttestedReadGate` era key) and provable at replay: a plan
|
|
12
|
+
* reads a DECLARED query of a FOREIGN workspace (`read(theQuery, args, { from })` + the directive's
|
|
13
|
+
* `.readsFrom(from, theQuery)`), the SOURCE holon's kernel signs an envelope binding the result to
|
|
14
|
+
* its chain position (head oid + certified fold-root + signer epoch), and every verify door checks
|
|
15
|
+
* SIGNATURE + TRUST + BOUNDS + FRESHNESS — never re-deriving from the foreign chain (the exact
|
|
16
|
+
* inverse of the same-workspace CAS lane; a foreign fact is carried evidence, not re-folded state).
|
|
17
|
+
*
|
|
18
|
+
* Stage 1 is the K1 layer: the reader's law PINS the accepted attestor keyHashes by hand
|
|
19
|
+
* ({@link trustSource}). K2 (`trustParent`, birth-cert-derived pins) and K3 (tracked rosters)
|
|
20
|
+
* grow the same declaration without re-shaping it (the `roster` mode names the layer).
|
|
21
|
+
*
|
|
22
|
+
* This file holds the halves that need `directive.ts` (the sanctioning ensure + the merge-seam
|
|
23
|
+
* injector); the leaf-module core lives in `attested_core.ts` (re-exported here).
|
|
24
|
+
*/
|
|
25
|
+
import { directive, type Directive } from "./directive.js";
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { NomosSourceFrontier } from "./attested_core.js";
|
|
28
|
+
|
|
29
|
+
export {
|
|
30
|
+
trustSource,
|
|
31
|
+
foreignQuery,
|
|
32
|
+
isTrustSourceDecl,
|
|
33
|
+
NomosSourceFrontier,
|
|
34
|
+
sourceFrontierId,
|
|
35
|
+
SOURCE_FRONTIER_PREFIX,
|
|
36
|
+
__trustFor,
|
|
37
|
+
type TrustSourceDecl,
|
|
38
|
+
type TrustSourceOptions,
|
|
39
|
+
type ForeignQueryRef,
|
|
40
|
+
} from "./attested_core.js";
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* THE FRONTIER SANCTIONING DECLARATION (the `nomosBirthRecordEnsure` pattern): the auto-appended
|
|
44
|
+
* `NomosSourceFrontier` event is an ENSURE at a deterministic id, so the id-mint gate admits it
|
|
45
|
+
* only through the declared-ensures lane — the law must carry an `Ensure → NomosSourceFrontier`
|
|
46
|
+
* directive. This one is DELIBERATELY USELESS TO DISPATCH (its plan writes nothing); its only
|
|
47
|
+
* load-bearing property is its manifest/USD presence.
|
|
48
|
+
*/
|
|
49
|
+
export const nomosSourceFrontierEnsure = directive("nomosSourceFrontierEnsure")
|
|
50
|
+
.ensures(NomosSourceFrontier)
|
|
51
|
+
.payload(z.object({}).strict())
|
|
52
|
+
.plan(() => []);
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Inject the attested-read structural exports into a merged module-exports bag IFF it declares ≥1
|
|
56
|
+
* foreign read (`.readsFrom`) — the RelationTuple/birth-meter "discoverable without re-exports"
|
|
57
|
+
* courtesy at the one merge seam. A foreign-read-free bag is returned UNTOUCHED (the era rule —
|
|
58
|
+
* byte-identical). Existing keys are never overwritten.
|
|
59
|
+
*/
|
|
60
|
+
export function withAttestedReadExports<M extends Record<string, unknown>>(mod: M): M {
|
|
61
|
+
const declaresForeign = Object.values(mod).some(
|
|
62
|
+
(v) =>
|
|
63
|
+
v !== null &&
|
|
64
|
+
typeof v === "object" &&
|
|
65
|
+
Array.isArray((v as { declaredForeignReads?: unknown }).declaredForeignReads) &&
|
|
66
|
+
((v as { declaredForeignReads: unknown[] }).declaredForeignReads.length > 0) &&
|
|
67
|
+
typeof (v as Directive<unknown>).plan === "function",
|
|
68
|
+
);
|
|
69
|
+
if (!declaresForeign) return mod;
|
|
70
|
+
const out: Record<string, unknown> = { ...mod };
|
|
71
|
+
if (!("NomosSourceFrontier" in out)) out.NomosSourceFrontier = NomosSourceFrontier;
|
|
72
|
+
if (!("nomosSourceFrontierEnsure" in out)) out.nomosSourceFrontierEnsure = nomosSourceFrontierEnsure;
|
|
73
|
+
return out as M;
|
|
74
|
+
}
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* ATTESTED READS — the CORE (leaf-module) half: the trust declaration, the foreign query
|
|
8
|
+
* reference, and the reader-side SourceFrontier aggregate. Kept free of `directive.ts` imports
|
|
9
|
+
* so `read.ts` (which `birth_meter.ts` → `directive.ts` depend on) can import it without a
|
|
10
|
+
* module-init cycle. The sanctioning ensure directive + the merge-seam injector live in
|
|
11
|
+
* `attested.ts`. Full doctrine: `architecture/attested_reads_design.md`.
|
|
12
|
+
*/
|
|
13
|
+
import { aggregate } from "./aggregate.js";
|
|
14
|
+
import { t } from "./fields.js";
|
|
15
|
+
import { Lww } from "./drivers.js";
|
|
16
|
+
|
|
17
|
+
/** A lightweight reference to a query declared in the SOURCE workspace's law — the reader
|
|
18
|
+
* consumes only its id (the query spec is the SOURCE's law, never re-declared reader-side).
|
|
19
|
+
* Use this (not a full `query(...)` export) so the foreign query never enters the READER's
|
|
20
|
+
* own read surface/openapi. A full QueryDecl is also accepted wherever this is. */
|
|
21
|
+
export interface ForeignQueryRef {
|
|
22
|
+
readonly id: string;
|
|
23
|
+
/** brand — keeps a foreign ref out of the reader's query auto-discovery. */
|
|
24
|
+
readonly __nomosForeignQuery: true;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Name a query of the SOURCE workspace's law by id (see {@link ForeignQueryRef}). */
|
|
28
|
+
export function foreignQuery(id: string): ForeignQueryRef {
|
|
29
|
+
return { id, __nomosForeignQuery: true };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** The §4.4 freshness + §1.3 key-resolution vocabulary of one declared trust edge. */
|
|
33
|
+
export interface TrustSourceOptions {
|
|
34
|
+
/** The source chain's GENESIS oid — the workspace's true identity (the name is routing). */
|
|
35
|
+
readonly genesis: string;
|
|
36
|
+
/** K1: the accepted read-attestor keyHashes (`sha256hex(pubkey)`). Required while `roster`
|
|
37
|
+
* is `"pinned"` (the stage-1 default); later layers derive/track the set. */
|
|
38
|
+
readonly keyHashes?: readonly string[];
|
|
39
|
+
/** §4.4a — wall-clock freshness bound in ms (judged against the intent's frozen HLC). */
|
|
40
|
+
readonly maxAgeMs?: number;
|
|
41
|
+
/** §4.4b — the epoch anchor: `"birth"` (the reader's own birth-cert epoch) or an integer floor. */
|
|
42
|
+
readonly minSourceEpoch?: "birth" | number;
|
|
43
|
+
/** §4.4c — maintain + enforce the monotone `SourceFrontier` (no regression below what this
|
|
44
|
+
* reader's chain has already seen from this source). */
|
|
45
|
+
readonly monotone?: boolean;
|
|
46
|
+
/** §1.3 — how the accepted-key set is populated: `"pinned"` (K1, acts in stage 1),
|
|
47
|
+
* `"derived"` (K2), `"tracked"` (K3). Parsed by the kernel from stage 1; later modes
|
|
48
|
+
* only ACT in later stages. */
|
|
49
|
+
readonly roster?: "pinned" | "derived" | "tracked";
|
|
50
|
+
/** §5.3 — what a folded `SourceDishonor` fact does at verify: `"refuse"` (default) or `"record"`. */
|
|
51
|
+
readonly onDishonor?: "refuse" | "record";
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One declared trust edge — an exported {@link trustSource} value, auto-discovered by shape. */
|
|
55
|
+
export interface TrustSourceDecl extends TrustSourceOptions {
|
|
56
|
+
readonly workspace: string;
|
|
57
|
+
readonly __nomosTrustSource: true;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
// ── the runtime trust registry — module-init state the foreign read lane consults ──────────────
|
|
61
|
+
// `trustSource()` runs at module evaluation BOTH at compile (node) and inside the sealed lump
|
|
62
|
+
// (QuickJS), so the read lane can resolve an edge's `monotone` posture without plumbing the
|
|
63
|
+
// declaration through every plan. Keyed by source workspace name; later declaration wins
|
|
64
|
+
// (the same later-wins the module merge applies).
|
|
65
|
+
const trustRegistry = new Map<string, TrustSourceDecl>();
|
|
66
|
+
|
|
67
|
+
/** The registered trust edge for a source workspace (`undefined` = undeclared). Library-internal. */
|
|
68
|
+
export function __trustFor(workspace: string): TrustSourceDecl | undefined {
|
|
69
|
+
return trustRegistry.get(workspace);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* DECLARE a trusted source workspace (the reader-law half of an attested read — §3.1). Export the
|
|
74
|
+
* returned decl from a domain module; `nomos-compile` discovers it by shape and lowers it into the
|
|
75
|
+
* `nomosAttestedReadGate` era key (emitted iff ≥1 directive declares a foreign read — a domain
|
|
76
|
+
* using none is byte-identical, the era rule).
|
|
77
|
+
*/
|
|
78
|
+
export function trustSource(workspace: string, opts: TrustSourceOptions): TrustSourceDecl {
|
|
79
|
+
const decl: TrustSourceDecl = { workspace, ...opts, __nomosTrustSource: true };
|
|
80
|
+
const roster = opts.roster ?? "pinned";
|
|
81
|
+
if (roster === "pinned" && (opts.keyHashes === undefined || opts.keyHashes.length === 0)) {
|
|
82
|
+
throw new Error(
|
|
83
|
+
`trustSource('${workspace}'): the 'pinned' roster (K1) requires ≥1 keyHash — pin the ` +
|
|
84
|
+
`source's read-attestor sha256hex(pubkey), or use roster:'derived'/'tracked' (later stages)`,
|
|
85
|
+
);
|
|
86
|
+
}
|
|
87
|
+
if (typeof opts.genesis !== "string" || opts.genesis.length === 0) {
|
|
88
|
+
throw new Error(
|
|
89
|
+
`trustSource('${workspace}'): 'genesis' (the source chain's genesis oid) is required — ` +
|
|
90
|
+
`the NAME is routing; genesis + key are identity`,
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
trustRegistry.set(workspace, decl);
|
|
94
|
+
return decl;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/** Shape predicate for compile discovery (the `isQueryDecl` pattern). */
|
|
98
|
+
export function isTrustSourceDecl(v: unknown): v is TrustSourceDecl {
|
|
99
|
+
return (
|
|
100
|
+
v !== null &&
|
|
101
|
+
typeof v === "object" &&
|
|
102
|
+
(v as { __nomosTrustSource?: unknown }).__nomosTrustSource === true &&
|
|
103
|
+
typeof (v as { workspace?: unknown }).workspace === "string"
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
// ── the SourceFrontier (§4.4c) — the reader-side monotone record of the source's position ──────
|
|
108
|
+
|
|
109
|
+
/** The deterministic instance-id prefix — `source-frontier:<source_genesis>` (keyed by the source's
|
|
110
|
+
* TRUE identity, never its routable name). Mirrored in the kernel
|
|
111
|
+
* (`nomos_executor::SOURCE_FRONTIER_PREFIX`). */
|
|
112
|
+
export const SOURCE_FRONTIER_PREFIX = "source-frontier:";
|
|
113
|
+
|
|
114
|
+
/** The frontier record's instance id for a source genesis. */
|
|
115
|
+
export function sourceFrontierId(sourceGenesis: string): string {
|
|
116
|
+
return SOURCE_FRONTIER_PREFIX + sourceGenesis;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* NomosSourceFrontier — the highest source position this reader's chain has SEEN per trusted
|
|
121
|
+
* source (§4.4c): the deterministic reduction of head-distance freshness. Updated IN-BAND by the
|
|
122
|
+
* foreign read lane (the slice-A birth-record pattern — the read lane emits it, the gate demands
|
|
123
|
+
* it); the gate's monotone arm refuses any attestation BEHIND it. Reader-chain state — re-folded
|
|
124
|
+
* at replay, so the predicate is deterministic forever.
|
|
125
|
+
*/
|
|
126
|
+
export const NomosSourceFrontier = aggregate("NomosSourceFrontier", {
|
|
127
|
+
/** The source workspace name the edge was declared under (routing/diagnostics). */
|
|
128
|
+
source: t.string().merge(Lww),
|
|
129
|
+
/** The highest source signer-epoch an admitted attestation has shown this reader. */
|
|
130
|
+
maxEpochSeen: t.int().merge(Lww),
|
|
131
|
+
/** The highest `attested_at_ms` an admitted attestation has shown this reader. */
|
|
132
|
+
maxAttestedAtSeen: t.int().merge(Lww),
|
|
133
|
+
/** The source head oid at the highest-seen position (diagnostics/audit). */
|
|
134
|
+
headAtMax: t.string().merge(Lww),
|
|
135
|
+
});
|
|
136
|
+
|
|
137
|
+
|
|
138
|
+
// ── the ensure() hook — breaks the read.ts → authoring.ts module-init cycle ────────────────────
|
|
139
|
+
// `authoring.ts` registers its `ensure()` here at module init; the foreign read lane calls it
|
|
140
|
+
// through this indirection (read.ts must not import authoring.ts: authoring → framework/relations
|
|
141
|
+
// → directive → birth_meter → read would be a cycle).
|
|
142
|
+
type EnsureFn = (
|
|
143
|
+
agg: unknown,
|
|
144
|
+
id: string,
|
|
145
|
+
) => { set(field: string, value: unknown): unknown };
|
|
146
|
+
let ensureImpl: EnsureFn | null = null;
|
|
147
|
+
/** Registered by `authoring.ts` at module init. Library-internal. */
|
|
148
|
+
export function __registerEnsure(fn: EnsureFn): void {
|
|
149
|
+
ensureImpl = fn;
|
|
150
|
+
}
|
|
151
|
+
/** The registered `ensure()` — throws NAMED if authoring never initialized (impossible in any
|
|
152
|
+
* real bundle: `read` and `ensure` ride the same library). Library-internal. */
|
|
153
|
+
export function __ensure(agg: unknown, id: string): { set(field: string, value: unknown): unknown } {
|
|
154
|
+
if (ensureImpl === null) {
|
|
155
|
+
throw new Error("attested read: the authoring surface is not initialized (no ensure())");
|
|
156
|
+
}
|
|
157
|
+
return ensureImpl(agg, id);
|
|
158
|
+
}
|
package/src/authoring.ts
CHANGED
|
@@ -28,6 +28,7 @@ import type { Field } from "./fields.js";
|
|
|
28
28
|
import type { FieldOp, PlannedOp, PlannedEventMarker } from "./ops.js";
|
|
29
29
|
import { endorsementOf } from "./principal_evidence.js";
|
|
30
30
|
import { __qualifyAggregateType } from "./framework/relations.js";
|
|
31
|
+
import { __registerEnsure } from "./attested_core.js";
|
|
31
32
|
|
|
32
33
|
// ── the per-dispatch authoring sink — library state the engine never sees (NOT a side channel out of
|
|
33
34
|
// the sandbox; it is internal to the directive runtime, drained synchronously by `executeDirectiveToIntent`). ──
|
|
@@ -141,3 +142,7 @@ export function existing(agg: AggregateHandle, id: string): AggregateRef {
|
|
|
141
142
|
export function ensure(agg: AggregateHandle, id: string): AggregateRef {
|
|
142
143
|
return new Ref(agg, id, "ensures");
|
|
143
144
|
}
|
|
145
|
+
|
|
146
|
+
// Register ensure() for the foreign read lane (attested reads — see attested_core.ts; the
|
|
147
|
+
// indirection exists purely to avoid a module-init import cycle through read.ts).
|
|
148
|
+
__registerEnsure((agg, id) => ensure(agg as AggregateHandle, id));
|
package/src/build_package.ts
CHANGED
|
@@ -60,6 +60,7 @@ import {
|
|
|
60
60
|
synthesizeBirthQuotaInvariants,
|
|
61
61
|
} from "./birth_meter.js";
|
|
62
62
|
import { withBirthMeterExports } from "./birth_meter_ensure.js";
|
|
63
|
+
import { withAttestedReadExports, isTrustSourceDecl, type TrustSourceDecl } from "./attested.js";
|
|
63
64
|
import type { RelationTypeDecl } from "./relation_schema.js";
|
|
64
65
|
import { domainHash, emitManifestBytes } from "./manifest.js";
|
|
65
66
|
import { collectValueObjectDescriptors } from "./value_object.js";
|
|
@@ -157,6 +158,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
157
158
|
// Ensure→NomosBirthRecord manifest declaration is what admits the auto-appended record through
|
|
158
159
|
// the id-mint gate's declared-ensures lane. Birth-free domains are byte-identical (era rule).
|
|
159
160
|
merged = withBirthMeterExports(merged);
|
|
161
|
+
// ATTESTED READS (slice B): a foreign-read-declaring domain gains the SourceFrontier
|
|
162
|
+
// aggregate + its sanctioning ensure at the same seam (era-safe: others byte-identical).
|
|
163
|
+
merged = withAttestedReadExports(merged);
|
|
160
164
|
|
|
161
165
|
const byId = new Map<string, AggregateHandle>();
|
|
162
166
|
for (const a of [...aggregatesOf(merged), ...(spec.extraAggregates ?? [])]) byId.set(a.id, a);
|
|
@@ -251,6 +255,15 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
251
255
|
// byte-identical in the canonical manifest/USD to before the key existed.
|
|
252
256
|
// Discover the sentinel AND its declared owner/cert locators (the kernel reads them off the era key, names
|
|
253
257
|
// neither). A bare marker omits the locators ⇒ the kernel's framework default applies (the era rule).
|
|
258
|
+
// ATTESTED READS (slice B): the module-level trustSource declarations, discovered by shape
|
|
259
|
+
// across the merged exports (the workspaceInvariants duck-typing). Later-wins by workspace.
|
|
260
|
+
const trustByWs = new Map<string, TrustSourceDecl>();
|
|
261
|
+
for (const v of Object.values(merged)) {
|
|
262
|
+
if (isTrustSourceDecl(v)) trustByWs.set(v.workspace, v);
|
|
263
|
+
}
|
|
264
|
+
const trustSources = [...trustByWs.values()].sort((a, b) =>
|
|
265
|
+
a.workspace < b.workspace ? -1 : a.workspace > b.workspace ? 1 : 0,
|
|
266
|
+
);
|
|
254
267
|
const pabMarker =
|
|
255
268
|
parentAttestedBirthMarker(merged) ??
|
|
256
269
|
spec.modules.map((m) => parentAttestedBirthMarker(m)).find((x) => x !== null) ??
|
|
@@ -295,6 +308,9 @@ export function composeDomainModule(spec: DomainModuleSpec): DomainModule {
|
|
|
295
308
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST: omit-when-false — a law that doesn't require parent-attested
|
|
296
309
|
// birth is byte-identical in the canonical manifest/USD (the nomosParentAttestedBirth era rule).
|
|
297
310
|
...(parentAttestedBirth ? { parentAttestedBirth } : {}),
|
|
311
|
+
// Omit-when-empty — the nomosAttestedReadGate era rule: a domain with no declared trust
|
|
312
|
+
// edge is byte-identical in the canonical manifest/USD.
|
|
313
|
+
...(trustSources.length > 0 ? { trustSources } : {}),
|
|
298
314
|
...(dartImports.length > 0 ? { dartImports } : {}),
|
|
299
315
|
...(spec.dartRefImports !== undefined && spec.dartRefImports.length > 0
|
|
300
316
|
? { dartRefImports: [...spec.dartRefImports] }
|
package/src/codegen_dart.ts
CHANGED
|
@@ -2789,6 +2789,14 @@ export interface DomainModule {
|
|
|
2789
2789
|
parentAttestedBirth?:
|
|
2790
2790
|
| boolean
|
|
2791
2791
|
| { ownerObject?: string; ownerField?: string; certObject?: string; certField?: string };
|
|
2792
|
+
/**
|
|
2793
|
+
* The domain's DECLARED TRUST EDGES (slice B — attested cross-workspace reads,
|
|
2794
|
+
* `architecture/attested_reads_design.md` §3): the exported `trustSource(ws, …)` declarations,
|
|
2795
|
+
* discovered by shape. Lowered — together with directives' `.readsFrom` tuples — into the
|
|
2796
|
+
* `nomosAttestedReadGate` USD era key (emitted iff ≥1 directive declares a foreign read).
|
|
2797
|
+
* ADDITIVE + OPTIONAL: a domain with no trust edge omits this and is byte-identical (era rule).
|
|
2798
|
+
*/
|
|
2799
|
+
trustSources?: import("./attested.js").TrustSourceDecl[];
|
|
2792
2800
|
/**
|
|
2793
2801
|
* The domain's NATIVE ZANZIBAR RELATION SCHEMA (`relation_schema.ts` — the userset-rewrite law,
|
|
2794
2802
|
* P1). Pure-data per-type relation rewrites lowered to the `nomosRelations` USD era key (NO JS on
|
|
@@ -1272,9 +1272,14 @@ async function main(): Promise<void> {
|
|
|
1272
1272
|
// `read()`'s body (dsl/src/read.ts — keep them byte-identical) and is tree-shaken
|
|
1273
1273
|
// out of read-free lumps.
|
|
1274
1274
|
const CAPTURED_READ_LUMP_MARKER = "nomos.read is not provided by this host";
|
|
1275
|
+
// Slice B: a FOREIGN (attested) read declaration ALSO opts the law into the read lane —
|
|
1276
|
+
// `read(q, args, {from})` is served from the verified injected envelope, so a law whose
|
|
1277
|
+
// only reads are foreign is just as declared as a local-read law.
|
|
1275
1278
|
const declaresCapturedReads = domainModules.some((m) =>
|
|
1276
1279
|
m.directives.some(
|
|
1277
|
-
(d) =>
|
|
1280
|
+
(d) =>
|
|
1281
|
+
((d as { declaredQueryReads?: unknown[] }).declaredQueryReads ?? []).length > 0 ||
|
|
1282
|
+
((d as { declaredForeignReads?: unknown[] }).declaredForeignReads ?? []).length > 0,
|
|
1278
1283
|
),
|
|
1279
1284
|
);
|
|
1280
1285
|
if (javascript.includes(CAPTURED_READ_LUMP_MARKER) && !declaresCapturedReads) {
|
package/src/directive.ts
CHANGED
|
@@ -174,6 +174,16 @@ export interface Directive<P = unknown> {
|
|
|
174
174
|
* OMITTED from the canonical manifest when empty (hash-stable for read-free directives).
|
|
175
175
|
*/
|
|
176
176
|
readonly declaredQueryReads?: QueryDecl[];
|
|
177
|
+
/**
|
|
178
|
+
* The directive's DECLARED FOREIGN (attested cross-workspace) reads (slice B —
|
|
179
|
+
* `architecture/attested_reads_design.md` §3.1): the `(queryId, from)` trust edges its
|
|
180
|
+
* `plan` may `read(theQuery, args, { from })` on the write path. The query is the SOURCE
|
|
181
|
+
* workspace's law (only its id is consumed here); the trust in the source is a separate
|
|
182
|
+
* module-level {@link trustSource} declaration. Lowered into the `nomosAttestedReadGate`
|
|
183
|
+
* era key. OPTIONAL + ADDITIVE; OMITTED when empty (a foreign-read-free directive is
|
|
184
|
+
* byte-identical — the era rule).
|
|
185
|
+
*/
|
|
186
|
+
readonly declaredForeignReads?: { queryId: string; from: string }[];
|
|
177
187
|
/**
|
|
178
188
|
* The directive's DECLARED emit boundary: the event types its `plan` may EMIT,
|
|
179
189
|
* each with an optional `max` bound. Part of the domain identity. Defaults to `{}`
|
|
@@ -313,6 +323,14 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
313
323
|
* before this existed. Returns a NEW requirable directive (non-destructive).
|
|
314
324
|
*/
|
|
315
325
|
reads(...reads: (string | QueryDecl)[]): RequirableDirective<P>;
|
|
326
|
+
/**
|
|
327
|
+
* Declare ONE FOREIGN (attested cross-workspace) read boundary (slice B): the plan may
|
|
328
|
+
* `read(theQuery, args, { from: sourceWs })` — served at author by the SOURCE holon's kernel
|
|
329
|
+
* as a SIGNED attestation, verified at every gate against the module's `trustSource(sourceWs,
|
|
330
|
+
* …)` declaration (signature / accepted key + genesis / bounds / freshness). Callable multiple
|
|
331
|
+
* times; deduped by `(queryId, from)`. Purely additive (the era rule).
|
|
332
|
+
*/
|
|
333
|
+
readsFrom(sourceWorkspace: string, query: { id: string }): RequirableDirective<P>;
|
|
316
334
|
/**
|
|
317
335
|
* Declare ONE entry of the directive's EMIT boundary: an `eventType` its `plan` may
|
|
318
336
|
* emit, with an optional `max` count bound. Callable MULTIPLE times to accumulate
|
|
@@ -653,6 +671,19 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
|
653
671
|
const mergedQueries = [...byId.values()].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
654
672
|
return makeRequirable({ ...base, declaredReads: merged, declaredQueryReads: mergedQueries });
|
|
655
673
|
},
|
|
674
|
+
readsFrom(sourceWorkspace: string, query: { id: string }): RequirableDirective<P> {
|
|
675
|
+
// ATTESTED CROSS-WORKSPACE READ (slice B): accumulate the (queryId, from) trust edge,
|
|
676
|
+
// dedup by pair, sort (only the SET is the contract — byte-determinism for the era key).
|
|
677
|
+
const prior = base.declaredForeignReads ?? [];
|
|
678
|
+
const key = (e: { queryId: string; from: string }) => `${e.from}\u0000${e.queryId}`;
|
|
679
|
+
const byKey = new Map<string, { queryId: string; from: string }>(prior.map((e) => [key(e), e]));
|
|
680
|
+
const entry = { queryId: query.id, from: sourceWorkspace };
|
|
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
|
+
);
|
|
685
|
+
return makeRequirable({ ...base, declaredForeignReads: merged });
|
|
686
|
+
},
|
|
656
687
|
emits(eventType: string, opts?: { max?: number }): RequirableDirective<P> {
|
|
657
688
|
// Accumulate one event type onto any prior emits; a `max` bound is recorded only
|
|
658
689
|
// when supplied (so `{}` vs `{max}` is a real contract difference).
|
package/src/engine_entry.ts
CHANGED
|
@@ -59,6 +59,7 @@ import { isRelationTypeDecl } from "./relation_schema.js";
|
|
|
59
59
|
import { synthesizePreconditionInvariants } from "./precondition.js";
|
|
60
60
|
import { synthesizeBirthQuotaInvariants } from "./birth_meter.js";
|
|
61
61
|
import { withBirthMeterExports } from "./birth_meter_ensure.js";
|
|
62
|
+
import { withAttestedReadExports } from "./attested.js";
|
|
62
63
|
import { generateOpenApiFragment, mergeOpenApiFragments, type OpenApiDoc } from "./codegen_openapi.js";
|
|
63
64
|
import type { QueryDecl } from "./query.js";
|
|
64
65
|
import type { AnyCount } from "./count.js";
|
|
@@ -249,7 +250,7 @@ function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports
|
|
|
249
250
|
// METERED BIRTH: a `.births()`-declaring domain gains the birth-meter structural exports
|
|
250
251
|
// (the sanctioning `nomosBirthRecordEnsure` + the meter aggregates) at this same seam, so
|
|
251
252
|
// the registry + `describe()` see the exact surface the composed manifest carries.
|
|
252
|
-
return withBirthMeterExports(expandCapabilityExports(merged));
|
|
253
|
+
return withAttestedReadExports(withBirthMeterExports(expandCapabilityExports(merged)));
|
|
253
254
|
}
|
|
254
255
|
|
|
255
256
|
/**
|
package/src/index.ts
CHANGED
|
@@ -120,6 +120,19 @@ export { create, existing, ensure, type AggregateRef } from "./authoring.js";
|
|
|
120
120
|
// `nomos-compile` REFUSES law that references read() (a named compile error, never a runtime halt).
|
|
121
121
|
// The captured-reads wave (task #58) wires the capability in and lifts the refusal.
|
|
122
122
|
export { read } from "./read.js";
|
|
123
|
+
// ATTESTED CROSS-WORKSPACE READS (slice B — attested_reads_design.md): the declared trust
|
|
124
|
+
// surface (`trustSource`), the foreign query reference, and the reader-side SourceFrontier.
|
|
125
|
+
export {
|
|
126
|
+
trustSource,
|
|
127
|
+
foreignQuery,
|
|
128
|
+
NomosSourceFrontier,
|
|
129
|
+
sourceFrontierId,
|
|
130
|
+
SOURCE_FRONTIER_PREFIX,
|
|
131
|
+
nomosSourceFrontierEnsure,
|
|
132
|
+
type TrustSourceDecl,
|
|
133
|
+
type TrustSourceOptions,
|
|
134
|
+
type ForeignQueryRef,
|
|
135
|
+
} from "./attested.js";
|
|
123
136
|
// THE BIRTH EFFECT (G3 — birth is a declared kernel primitive): a `.births()` directive's plan records the
|
|
124
137
|
// child's genesis recipe via `birth(recipe)` (reuse `buildGenesisChain`); it rides the committed intent as a
|
|
125
138
|
// declared output, and the kernel — reading the `.births` declaration off the law manifest (the
|
package/src/read.ts
CHANGED
|
@@ -18,7 +18,8 @@
|
|
|
18
18
|
* routes them here. (See `docs/aggregate_lifecycle_and_relations.md`.)
|
|
19
19
|
*/
|
|
20
20
|
import type { QueryDecl } from "./query.js";
|
|
21
|
-
import type { WireRead } from "./wire.js";
|
|
21
|
+
import type { WireRead, WireReadAttestation } from "./wire.js";
|
|
22
|
+
import { NomosSourceFrontier, sourceFrontierId, __trustFor, __ensure } from "./attested_core.js";
|
|
22
23
|
|
|
23
24
|
// ── the per-dispatch read footprint — library state drained by `executeDirectiveToIntent` (mirrors the authoring sink) ──
|
|
24
25
|
let footprint: WireRead[] = [];
|
|
@@ -58,7 +59,11 @@ function host(): NomosRead {
|
|
|
58
59
|
* {@link CAPTURED_READ_LUMP_MARKER}, which lives ONLY in this function's body and is tree-shaken
|
|
59
60
|
* away with it) — and an inert host (no `nomos.read`) still halts with the named error below.
|
|
60
61
|
*/
|
|
61
|
-
export function read<T = unknown>(
|
|
62
|
+
export function read<T = unknown>(
|
|
63
|
+
query: QueryDecl | { id: string },
|
|
64
|
+
args: Record<string, string>,
|
|
65
|
+
opts?: { from?: string },
|
|
66
|
+
): T {
|
|
62
67
|
const h = host() as { read?: unknown } | undefined;
|
|
63
68
|
if (typeof h?.read !== "function") {
|
|
64
69
|
// The string below IS the compile gate's marker — keep it byte-identical to
|
|
@@ -69,9 +74,43 @@ export function read<T = unknown>(query: QueryDecl, args: Record<string, string>
|
|
|
69
74
|
`to the directive and recompile (#58).`,
|
|
70
75
|
);
|
|
71
76
|
}
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
77
|
+
const from = opts?.from ?? "";
|
|
78
|
+
if (from === "") {
|
|
79
|
+
const result = host().read(query.id, args) as T;
|
|
80
|
+
footprint.push({ queryId: query.id, args, result });
|
|
81
|
+
return result;
|
|
82
|
+
}
|
|
83
|
+
// ── THE FOREIGN (attested cross-workspace) LANE (slice B — attested_reads_design.md §4.1).
|
|
84
|
+
// The port answers the `{rows, attestation}` wrapper: at AUTHOR the kernel verified the
|
|
85
|
+
// injected source envelope before serving it; at VERIFY the recorded capture replays
|
|
86
|
+
// verbatim. The rows return to the plan; the envelope rides the footprint; and when the
|
|
87
|
+
// trust edge declares `monotone`, the lane records the advanced SourceFrontier IN-BAND
|
|
88
|
+
// (the slice-A birth-record pattern — the gate DEMANDS this record, so honestly-compiled
|
|
89
|
+
// law satisfies it by construction).
|
|
90
|
+
const answer = (host() as { read(q: string, a: Record<string, string>, o?: { from?: string }): unknown }).read(
|
|
91
|
+
query.id,
|
|
92
|
+
args,
|
|
93
|
+
{ from },
|
|
94
|
+
) as { rows: T; attestation: WireReadAttestation };
|
|
95
|
+
const att = answer?.attestation;
|
|
96
|
+
if (att === undefined || att === null) {
|
|
97
|
+
throw new Error(
|
|
98
|
+
`read('${query.id}', {from: '${from}'}): the read port answered no attestation — ` +
|
|
99
|
+
`a foreign read is served only as a signed envelope (declare .readsFrom + trustSource ` +
|
|
100
|
+
`and author through a holon door that injects the source attestation).`,
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
footprint.push({ queryId: query.id, args, result: answer.rows, attestation: att });
|
|
104
|
+
if (__trustFor(from)?.monotone === true) {
|
|
105
|
+
const frontier = __ensure(NomosSourceFrontier, sourceFrontierId(att.source_genesis)) as {
|
|
106
|
+
set(f: string, v: unknown): unknown;
|
|
107
|
+
};
|
|
108
|
+
frontier.set("source", from);
|
|
109
|
+
frontier.set("maxEpochSeen", att.source_epoch);
|
|
110
|
+
frontier.set("maxAttestedAtSeen", att.attested_at_ms);
|
|
111
|
+
frontier.set("headAtMax", att.source_head);
|
|
112
|
+
}
|
|
113
|
+
return answer.rows;
|
|
75
114
|
}
|
|
76
115
|
|
|
77
116
|
/**
|
|
@@ -55,7 +55,20 @@ export function qualifyManifestAggregateLabels(
|
|
|
55
55
|
return {
|
|
56
56
|
...manifest,
|
|
57
57
|
aggregates: manifest.aggregates.map((a) => (namespaced.has(a.id) ? { ...a, id: qa(a.id) } : a)),
|
|
58
|
-
|
|
58
|
+
// NB: `capturedReads[].returns` (the #58 read-gate spec the kernel derives against) MUST be
|
|
59
|
+
// qualified too — the projection stamps the QUALIFIED `__type` on every row, so a bare
|
|
60
|
+
// `returns` derives 0 rows at the one gate (the bug that broke captured_reads.test.mjs).
|
|
61
|
+
directives: manifest.directives.map((d) => ({
|
|
62
|
+
...d,
|
|
63
|
+
...(namespaced.has(d.target) ? { target: qa(d.target) } : {}),
|
|
64
|
+
...(d.capturedReads !== undefined
|
|
65
|
+
? {
|
|
66
|
+
capturedReads: d.capturedReads.map((r) =>
|
|
67
|
+
namespaced.has(r.returns) ? { ...r, returns: qa(r.returns) } : r,
|
|
68
|
+
),
|
|
69
|
+
}
|
|
70
|
+
: {}),
|
|
71
|
+
})),
|
|
59
72
|
...(stableIds !== undefined ? { stableIds } : {}),
|
|
60
73
|
...(manifest.queries !== undefined
|
|
61
74
|
? { queries: manifest.queries.map((q) => (namespaced.has(q.returns) ? { ...q, returns: qa(q.returns) } : q)) }
|
package/src/shape_compat.ts
CHANGED
|
@@ -54,6 +54,10 @@ 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 `(queryId, from)` trust edges —
|
|
58
|
+
* the cross-workspace dependencies this law consumes on its write path. OMITTED when the
|
|
59
|
+
* law declares none (the era rule). */
|
|
60
|
+
readonly foreignReads?: Record<string, { queryId: string; from: string }[]>;
|
|
57
61
|
}
|
|
58
62
|
|
|
59
63
|
/** The verdict: compatible iff nothing is missing and nothing is type-incompatible.
|
|
@@ -147,12 +151,23 @@ export function domainInterface(mod: DomainModule, stableIds?: StableIds): Domai
|
|
|
147
151
|
aggEntries.push([aggSid, { sid: aggSid, label, fields: sortedRec(fieldEntries) }]);
|
|
148
152
|
}
|
|
149
153
|
Object.assign(aggregates, sortedRec(aggEntries));
|
|
154
|
+
// ATTESTED READS (slice B): the declared trust edges — so shape-compat + client codegen can
|
|
155
|
+
// SEE the cross-workspace dependency (typed clients don't call it; it's write-path-internal).
|
|
156
|
+
const foreignReadEntries: Array<[string, { queryId: string; from: string }[]]> = [];
|
|
157
|
+
for (const d of (mod.directives ?? []) as Array<{ id?: unknown; declaredForeignReads?: { queryId: string; from: string }[] }>) {
|
|
158
|
+
if (!d || typeof d.id !== "string") continue;
|
|
159
|
+
if (d.declaredForeignReads !== undefined && d.declaredForeignReads.length > 0) {
|
|
160
|
+
foreignReadEntries.push([d.id, d.declaredForeignReads.map((e) => ({ ...e }))]);
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
const foreignReads = sortedRec(foreignReadEntries);
|
|
150
164
|
return {
|
|
151
165
|
v: 1,
|
|
152
166
|
domain: (mod as { domain?: string; name?: string }).domain ?? (mod as { name?: string }).name ?? "",
|
|
153
167
|
...(stableIds?.domain !== undefined ? { domainSid: stableIds.domain } : {}),
|
|
154
168
|
directives,
|
|
155
169
|
aggregates,
|
|
170
|
+
...(foreignReadEntries.length > 0 ? { foreignReads } : {}),
|
|
156
171
|
};
|
|
157
172
|
}
|
|
158
173
|
|
package/src/usd.ts
CHANGED
|
@@ -433,6 +433,34 @@ export interface UsdLayer {
|
|
|
433
433
|
/** directive id → the SORTED query ids its plan may read. */
|
|
434
434
|
readonly directives: Record<string, string[]>;
|
|
435
435
|
};
|
|
436
|
+
/**
|
|
437
|
+
* THE ATTESTED-READ GATE DECLARATION (slice B — attested cross-workspace reads,
|
|
438
|
+
* `architecture/attested_reads_design.md` §3.2). Present IFF ≥1 directive declares a
|
|
439
|
+
* foreign read (`.readsFrom(ws, query)` + a module-level `trustSource(ws, …)`): the
|
|
440
|
+
* reader's law names, per trusted source, the source's IDENTITY (genesis), the accepted
|
|
441
|
+
* attestor keyHashes (K1 pins), and its freshness/dishonor vocabulary; and per directive,
|
|
442
|
+
* the `(queryId, from)` edges its plan may consume. The QUERY SPEC is NOT emitted here —
|
|
443
|
+
* the query is the SOURCE's law; the reader declares only the trust edge + the id.
|
|
444
|
+
* OMITTED for foreign-read-free law (byte-identical hash — the exact `nomosReadGate` era
|
|
445
|
+
* discipline). The `roster` mode names the §1.3 layer (`pinned`=K1, `derived`=K2,
|
|
446
|
+
* `tracked`=K3) — one era key from stage 1; later stages ADD modes, never re-shape.
|
|
447
|
+
*/
|
|
448
|
+
readonly nomosAttestedReadGate?: {
|
|
449
|
+
readonly v: 1;
|
|
450
|
+
readonly sources: Record<
|
|
451
|
+
string,
|
|
452
|
+
{
|
|
453
|
+
genesis: string;
|
|
454
|
+
keyHashes?: string[];
|
|
455
|
+
maxAgeMs?: number;
|
|
456
|
+
minSourceEpoch?: "birth" | number;
|
|
457
|
+
monotone?: boolean;
|
|
458
|
+
roster: "pinned" | "derived" | "tracked";
|
|
459
|
+
onDishonor: "refuse" | "record";
|
|
460
|
+
}
|
|
461
|
+
>;
|
|
462
|
+
readonly directives: Record<string, { queryId: string; from: string }[]>;
|
|
463
|
+
};
|
|
436
464
|
/**
|
|
437
465
|
* THE READ-VISIBILITY GATE DECLARATION (the native-relations read gate).
|
|
438
466
|
* Present IFF ≥1 aggregate of this layer is declared PRIVATE (`visibility:
|
|
@@ -952,6 +980,52 @@ export function emitUsd(
|
|
|
952
980
|
}
|
|
953
981
|
const nomosAttestationGate =
|
|
954
982
|
Object.keys(attestationReq).length > 0 ? { v: 1 as const, directives: attestationReq } : undefined;
|
|
983
|
+
// THE ATTESTED-READ GATE (slice B): per-directive `(queryId, from)` edges + the module's
|
|
984
|
+
// trustSource declarations. Emitted iff ≥1 directive declares a foreign read (the era
|
|
985
|
+
// rule). A declared `from` with NO matching trustSource is a NAMED COMPILE ERROR — the
|
|
986
|
+
// kernel would fail it closed anyway; refuse loudly at build instead.
|
|
987
|
+
const foreignDirectives: Record<string, { queryId: string; from: string }[]> = {};
|
|
988
|
+
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?: { queryId: string; from: string }[] }).declaredForeignReads;
|
|
990
|
+
if (fr !== undefined && fr.length > 0) foreignDirectives[d.id] = fr.map((e) => ({ ...e }));
|
|
991
|
+
}
|
|
992
|
+
let nomosAttestedReadGate: UsdLayer["nomosAttestedReadGate"];
|
|
993
|
+
if (Object.keys(foreignDirectives).length > 0) {
|
|
994
|
+
const trust = (l.module as { trustSources?: readonly import("./attested.js").TrustSourceDecl[] }).trustSources ?? [];
|
|
995
|
+
const trustByWs = new Map(trust.map((tsrc) => [tsrc.workspace, tsrc]));
|
|
996
|
+
const sourcesRec: NonNullable<UsdLayer["nomosAttestedReadGate"]>["sources"] = {};
|
|
997
|
+
for (const [dirId, edges] of Object.entries(foreignDirectives)) {
|
|
998
|
+
for (const e of edges) {
|
|
999
|
+
const tsrc = trustByWs.get(e.from);
|
|
1000
|
+
if (tsrc === undefined) {
|
|
1001
|
+
throw new Error(
|
|
1002
|
+
`directive '${dirId}' declares .readsFrom('${e.from}', '${e.queryId}') but the ` +
|
|
1003
|
+
`module exports no trustSource('${e.from}', …) — the trust edge must be declared ` +
|
|
1004
|
+
`in law (attested_reads_design.md §3.1)`,
|
|
1005
|
+
);
|
|
1006
|
+
}
|
|
1007
|
+
if (sourcesRec[e.from] === undefined) {
|
|
1008
|
+
sourcesRec[e.from] = {
|
|
1009
|
+
genesis: tsrc.genesis,
|
|
1010
|
+
...(tsrc.keyHashes !== undefined && tsrc.keyHashes.length > 0
|
|
1011
|
+
? { keyHashes: [...tsrc.keyHashes].sort() }
|
|
1012
|
+
: {}),
|
|
1013
|
+
...(tsrc.maxAgeMs !== undefined ? { maxAgeMs: tsrc.maxAgeMs } : {}),
|
|
1014
|
+
...(tsrc.minSourceEpoch !== undefined ? { minSourceEpoch: tsrc.minSourceEpoch } : {}),
|
|
1015
|
+
...(tsrc.monotone === true ? { monotone: true } : {}),
|
|
1016
|
+
roster: tsrc.roster ?? "pinned",
|
|
1017
|
+
onDishonor: tsrc.onDishonor ?? "refuse",
|
|
1018
|
+
};
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
}
|
|
1022
|
+
// SORTED-KEY records (byte-determinism — matching the layered flatten).
|
|
1023
|
+
const sortedSources: typeof sourcesRec = {};
|
|
1024
|
+
for (const k of Object.keys(sourcesRec).sort()) sortedSources[k] = sourcesRec[k]!;
|
|
1025
|
+
const sortedDirectives: Record<string, { queryId: string; from: string }[]> = {};
|
|
1026
|
+
for (const k of Object.keys(foreignDirectives).sort()) sortedDirectives[k] = foreignDirectives[k]!;
|
|
1027
|
+
nomosAttestedReadGate = { v: 1 as const, sources: sortedSources, directives: sortedDirectives };
|
|
1028
|
+
}
|
|
955
1029
|
// THE BIRTH GATE (G3 — birth is a declared kernel primitive): the SORTED set of directive ids
|
|
956
1030
|
// declaring `.births()`. The kernel reads this off the manifest (never a domain aggregate) to know an
|
|
957
1031
|
// admitted directive births, then folds the recipe the intent carried. Omit-when-empty (era rule —
|
|
@@ -1160,6 +1234,7 @@ export function emitUsd(
|
|
|
1160
1234
|
...(nomosReadProjectionNamespace !== undefined ? { nomosReadProjectionNamespace } : {}),
|
|
1161
1235
|
...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
|
|
1162
1236
|
...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
|
|
1237
|
+
...(nomosAttestedReadGate !== undefined ? { nomosAttestedReadGate } : {}),
|
|
1163
1238
|
...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
|
|
1164
1239
|
...(nomosBirthRecordGate !== undefined ? { nomosBirthRecordGate } : {}),
|
|
1165
1240
|
...(nomosParentAttestedBirth !== undefined ? { nomosParentAttestedBirth } : {}),
|
package/src/usd_layers.ts
CHANGED
|
@@ -185,6 +185,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
|
|
|
185
185
|
...(layer.nomosAttestationGate !== undefined
|
|
186
186
|
? [`${indent}custom string nomos:attestationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestationGate)))}`]
|
|
187
187
|
: []),
|
|
188
|
+
...(layer.nomosAttestedReadGate !== undefined
|
|
189
|
+
? [`${indent}custom string nomos:attestedReadGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestedReadGate)))}`]
|
|
190
|
+
: []),
|
|
188
191
|
...(layer.nomosBirthGate !== undefined
|
|
189
192
|
? [`${indent}custom string nomos:birthGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosBirthGate)))}`]
|
|
190
193
|
: []),
|
|
@@ -828,6 +831,26 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
828
831
|
attestationGate = { v: 1, directives: rec };
|
|
829
832
|
}
|
|
830
833
|
}
|
|
834
|
+
// THE ATTESTED-READ GATE (slice B): sources + per-directive edges merged later-wins across
|
|
835
|
+
// layers, SORTED keys (matching the composed emission). Omit-when-empty (era rule).
|
|
836
|
+
let attestedReadGate: UsdLayer["nomosAttestedReadGate"];
|
|
837
|
+
{
|
|
838
|
+
type SrcSpec = NonNullable<UsdLayer["nomosAttestedReadGate"]>["sources"][string];
|
|
839
|
+
const srcs = new Map<string, SrcSpec>();
|
|
840
|
+
const dirs = new Map<string, { queryId: string; from: string }[]>();
|
|
841
|
+
for (const l of stack) {
|
|
842
|
+
if (l.nomosAttestedReadGate === undefined) continue;
|
|
843
|
+
for (const [ws, spec] of Object.entries(l.nomosAttestedReadGate.sources)) srcs.set(ws, spec);
|
|
844
|
+
for (const [id, edges] of Object.entries(l.nomosAttestedReadGate.directives)) dirs.set(id, edges);
|
|
845
|
+
}
|
|
846
|
+
if (dirs.size > 0) {
|
|
847
|
+
const sourcesRec: Record<string, SrcSpec> = {};
|
|
848
|
+
for (const k of [...srcs.keys()].sort()) sourcesRec[k] = srcs.get(k)!;
|
|
849
|
+
const directivesRec: Record<string, { queryId: string; from: string }[]> = {};
|
|
850
|
+
for (const k of [...dirs.keys()].sort()) directivesRec[k] = dirs.get(k)!;
|
|
851
|
+
attestedReadGate = { v: 1, sources: sourcesRec, directives: directivesRec };
|
|
852
|
+
}
|
|
853
|
+
}
|
|
831
854
|
// THE BIRTH GATE (G3): the SORTED union of every layer's `.births()` directive ids. Omit-when-empty
|
|
832
855
|
// (era rule). The set is the contract; order is incidental (sorted for byte-determinism).
|
|
833
856
|
let birthGate: UsdLayer["nomosBirthGate"];
|
|
@@ -916,6 +939,7 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
916
939
|
...(readProjectionNamespace !== undefined ? { nomosReadProjectionNamespace: readProjectionNamespace } : {}),
|
|
917
940
|
...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
|
|
918
941
|
...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
|
|
942
|
+
...(attestedReadGate !== undefined ? { nomosAttestedReadGate: attestedReadGate } : {}),
|
|
919
943
|
...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
|
|
920
944
|
...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
|
|
921
945
|
...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
|
|
@@ -1053,6 +1077,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1053
1077
|
const relationNamespaceByDomain = new Map<string, UsdLayer["nomosRelationNamespace"]>();
|
|
1054
1078
|
const readProjectionNamespaceByDomain = new Map<string, UsdLayer["nomosReadProjectionNamespace"]>();
|
|
1055
1079
|
const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
|
|
1080
|
+
const attestedReadGateByDomain = new Map<string, UsdLayer["nomosAttestedReadGate"]>();
|
|
1056
1081
|
const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
|
|
1057
1082
|
const birthRecordGateByDomain = new Map<string, UsdLayer["nomosBirthRecordGate"]>();
|
|
1058
1083
|
const parentAttestedBirthByDomain = new Map<string, UsdLayer["nomosParentAttestedBirth"]>();
|
|
@@ -1119,6 +1144,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1119
1144
|
if (typeof attGateHex === "string") {
|
|
1120
1145
|
attestationGateByDomain.set(domain, JSON.parse(unhexUtf8(attGateHex)) as UsdLayer["nomosAttestationGate"]);
|
|
1121
1146
|
}
|
|
1147
|
+
// THE ATTESTED-READ GATE (slice B): the hex-carried trust-edge declaration.
|
|
1148
|
+
const attReadGateHex = p.attrs.get("nomos:attestedReadGateHex");
|
|
1149
|
+
if (typeof attReadGateHex === "string") {
|
|
1150
|
+
attestedReadGateByDomain.set(domain, JSON.parse(unhexUtf8(attReadGateHex)) as UsdLayer["nomosAttestedReadGate"]);
|
|
1151
|
+
}
|
|
1122
1152
|
// THE BIRTH GATE (G3): the hex-carried `.births()` directive set.
|
|
1123
1153
|
const birthGateHex = p.attrs.get("nomos:birthGateHex");
|
|
1124
1154
|
if (typeof birthGateHex === "string") {
|
|
@@ -1243,6 +1273,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1243
1273
|
const relNs = relationNamespaceByDomain.get(domain);
|
|
1244
1274
|
const rpNs = readProjectionNamespaceByDomain.get(domain);
|
|
1245
1275
|
const attGate = attestationGateByDomain.get(domain);
|
|
1276
|
+
const attReadGate = attestedReadGateByDomain.get(domain);
|
|
1246
1277
|
const birthGate = birthGateByDomain.get(domain);
|
|
1247
1278
|
const birthRecordGate = birthRecordGateByDomain.get(domain);
|
|
1248
1279
|
const parentAttestedBirth = parentAttestedBirthByDomain.get(domain);
|
|
@@ -1266,6 +1297,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1266
1297
|
...(rpNs !== undefined ? { nomosReadProjectionNamespace: rpNs } : {}),
|
|
1267
1298
|
...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
|
|
1268
1299
|
...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
|
|
1300
|
+
...(attReadGate !== undefined ? { nomosAttestedReadGate: attReadGate } : {}),
|
|
1269
1301
|
...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
|
|
1270
1302
|
...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
|
|
1271
1303
|
...(parentAttestedBirth !== undefined ? { nomosParentAttestedBirth: parentAttestedBirth } : {}),
|
package/src/wire.ts
CHANGED
|
@@ -162,6 +162,42 @@ export interface WireRead {
|
|
|
162
162
|
args: Record<string, string>;
|
|
163
163
|
/** The captured result. Replay returns this verbatim; the gate may re-verify it against `hlc`. */
|
|
164
164
|
result: unknown;
|
|
165
|
+
/**
|
|
166
|
+
* THE READ ATTESTATION (slice B — attested cross-workspace reads): present IFF this read was
|
|
167
|
+
* served by a FOREIGN workspace (`read(q, args, {from})`). The TS twin of the kernel's
|
|
168
|
+
* `ports::ReadAttestation` (snake_case — the kernel serde wire). OMITTED for a local read so
|
|
169
|
+
* a local-read intent stays byte-identical (the era rule).
|
|
170
|
+
*/
|
|
171
|
+
attestation?: WireReadAttestation;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
/**
|
|
175
|
+
* The kernel `ports::ReadAttestation` wire twin (attested_reads_design.md §2.1) — FIXED at its
|
|
176
|
+
* terminal shape: the signed core fields + three optional evidence slots later stages populate
|
|
177
|
+
* (`delegation` K2, `roster_proof` K3, `receipt_hint` receipts). Field names are the kernel's
|
|
178
|
+
* serde snake_case (this object rides INSIDE `captured_ports.reads[i]`).
|
|
179
|
+
*/
|
|
180
|
+
export interface WireReadAttestation {
|
|
181
|
+
v: number;
|
|
182
|
+
source_workspace: string;
|
|
183
|
+
source_genesis: string;
|
|
184
|
+
source_head: string;
|
|
185
|
+
fold_root: string;
|
|
186
|
+
source_epoch: number;
|
|
187
|
+
attested_at_ms: number;
|
|
188
|
+
/** base64(32B ed25519 public key) — keyHash = sha256hex(raw). */
|
|
189
|
+
public_key: string;
|
|
190
|
+
/** base64(64B ed25519) over the `nomos-attested-read-v1\0` signed bytes. */
|
|
191
|
+
signature: string;
|
|
192
|
+
delegation?: {
|
|
193
|
+
signer_key_hash: string;
|
|
194
|
+
attestor_key_hash: string;
|
|
195
|
+
epoch: number;
|
|
196
|
+
delegated_at_ms: number;
|
|
197
|
+
signature: string;
|
|
198
|
+
};
|
|
199
|
+
roster_proof?: { commit_b64: string; leaf_b64: string; path: string[] };
|
|
200
|
+
receipt_hint?: { batch_id: string };
|
|
165
201
|
}
|
|
166
202
|
|
|
167
203
|
/**
|