@githolon/dsl 0.34.1 → 0.36.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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.34.1",
3
+ "version": "0.36.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",
@@ -0,0 +1,76 @@
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
+ trustParent,
32
+ PARENT_TRUST_SENTINEL,
33
+ foreignQuery,
34
+ isTrustSourceDecl,
35
+ NomosSourceFrontier,
36
+ sourceFrontierId,
37
+ SOURCE_FRONTIER_PREFIX,
38
+ __trustFor,
39
+ type TrustSourceDecl,
40
+ type TrustSourceOptions,
41
+ type ForeignQueryRef,
42
+ } from "./attested_core.js";
43
+
44
+ /**
45
+ * THE FRONTIER SANCTIONING DECLARATION (the `nomosBirthRecordEnsure` pattern): the auto-appended
46
+ * `NomosSourceFrontier` event is an ENSURE at a deterministic id, so the id-mint gate admits it
47
+ * only through the declared-ensures lane — the law must carry an `Ensure → NomosSourceFrontier`
48
+ * directive. This one is DELIBERATELY USELESS TO DISPATCH (its plan writes nothing); its only
49
+ * load-bearing property is its manifest/USD presence.
50
+ */
51
+ export const nomosSourceFrontierEnsure = directive("nomosSourceFrontierEnsure")
52
+ .ensures(NomosSourceFrontier)
53
+ .payload(z.object({}).strict())
54
+ .plan(() => []);
55
+
56
+ /**
57
+ * Inject the attested-read structural exports into a merged module-exports bag IFF it declares ≥1
58
+ * foreign read (`.readsFrom`) — the RelationTuple/birth-meter "discoverable without re-exports"
59
+ * courtesy at the one merge seam. A foreign-read-free bag is returned UNTOUCHED (the era rule —
60
+ * byte-identical). Existing keys are never overwritten.
61
+ */
62
+ export function withAttestedReadExports<M extends Record<string, unknown>>(mod: M): M {
63
+ const declaresForeign = Object.values(mod).some(
64
+ (v) =>
65
+ v !== null &&
66
+ typeof v === "object" &&
67
+ Array.isArray((v as { declaredForeignReads?: unknown }).declaredForeignReads) &&
68
+ ((v as { declaredForeignReads: unknown[] }).declaredForeignReads.length > 0) &&
69
+ typeof (v as Directive<unknown>).plan === "function",
70
+ );
71
+ if (!declaresForeign) return mod;
72
+ const out: Record<string, unknown> = { ...mod };
73
+ if (!("NomosSourceFrontier" in out)) out.NomosSourceFrontier = NomosSourceFrontier;
74
+ if (!("nomosSourceFrontierEnsure" in out)) out.nomosSourceFrontierEnsure = nomosSourceFrontierEnsure;
75
+ return out as M;
76
+ }
@@ -0,0 +1,179 @@
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
+ /** K2's genesis sentinel — `trustParent()` declares `genesis: "@parent"`; the kernel derives
98
+ * identity from the reader's OWN birth certificate (kernel mirror:
99
+ * `nomos_executor::PARENT_TRUST_SENTINEL`). */
100
+ export const PARENT_TRUST_SENTINEL = "@parent";
101
+
102
+ /**
103
+ * DECLARE trust in the reader's PARENT (K2, stage 4 — §1.3): the accepted attestor set is
104
+ * DERIVED from the reader's own birth certificate (`BirthAttestation.parentKey`, seeded at
105
+ * birth by the parent's offer-effect) plus any delegation cert the envelope carries that the
106
+ * parent key signed — zero hand-pinned keys, one-hop vouching by construction. `workspace`
107
+ * remains the ROUTABLE name (transport must know whom to ask); identity is the derived chain,
108
+ * never the name. Rotation = the source serves a fresh parent-signed delegation — no reader
109
+ * law change ever.
110
+ */
111
+ export function trustParent(
112
+ workspace: string,
113
+ opts: Omit<TrustSourceOptions, "genesis" | "keyHashes" | "roster"> = {},
114
+ ): TrustSourceDecl {
115
+ return trustSource(workspace, { ...opts, genesis: PARENT_TRUST_SENTINEL, roster: "derived" });
116
+ }
117
+
118
+ /** Shape predicate for compile discovery (the `isQueryDecl` pattern). */
119
+ export function isTrustSourceDecl(v: unknown): v is TrustSourceDecl {
120
+ return (
121
+ v !== null &&
122
+ typeof v === "object" &&
123
+ (v as { __nomosTrustSource?: unknown }).__nomosTrustSource === true &&
124
+ typeof (v as { workspace?: unknown }).workspace === "string"
125
+ );
126
+ }
127
+
128
+ // ── the SourceFrontier (§4.4c) — the reader-side monotone record of the source's position ──────
129
+
130
+ /** The deterministic instance-id prefix — `source-frontier:<source_genesis>` (keyed by the source's
131
+ * TRUE identity, never its routable name). Mirrored in the kernel
132
+ * (`nomos_executor::SOURCE_FRONTIER_PREFIX`). */
133
+ export const SOURCE_FRONTIER_PREFIX = "source-frontier:";
134
+
135
+ /** The frontier record's instance id for a source genesis. */
136
+ export function sourceFrontierId(sourceGenesis: string): string {
137
+ return SOURCE_FRONTIER_PREFIX + sourceGenesis;
138
+ }
139
+
140
+ /**
141
+ * NomosSourceFrontier — the highest source position this reader's chain has SEEN per trusted
142
+ * source (§4.4c): the deterministic reduction of head-distance freshness. Updated IN-BAND by the
143
+ * foreign read lane (the slice-A birth-record pattern — the read lane emits it, the gate demands
144
+ * it); the gate's monotone arm refuses any attestation BEHIND it. Reader-chain state — re-folded
145
+ * at replay, so the predicate is deterministic forever.
146
+ */
147
+ export const NomosSourceFrontier = aggregate("NomosSourceFrontier", {
148
+ /** The source workspace name the edge was declared under (routing/diagnostics). */
149
+ source: t.string().merge(Lww),
150
+ /** The highest source signer-epoch an admitted attestation has shown this reader. */
151
+ maxEpochSeen: t.int().merge(Lww),
152
+ /** The highest `attested_at_ms` an admitted attestation has shown this reader. */
153
+ maxAttestedAtSeen: t.int().merge(Lww),
154
+ /** The source head oid at the highest-seen position (diagnostics/audit). */
155
+ headAtMax: t.string().merge(Lww),
156
+ });
157
+
158
+
159
+ // ── the ensure() hook — breaks the read.ts → authoring.ts module-init cycle ────────────────────
160
+ // `authoring.ts` registers its `ensure()` here at module init; the foreign read lane calls it
161
+ // through this indirection (read.ts must not import authoring.ts: authoring → framework/relations
162
+ // → directive → birth_meter → read would be a cycle).
163
+ type EnsureFn = (
164
+ agg: unknown,
165
+ id: string,
166
+ ) => { set(field: string, value: unknown): unknown };
167
+ let ensureImpl: EnsureFn | null = null;
168
+ /** Registered by `authoring.ts` at module init. Library-internal. */
169
+ export function __registerEnsure(fn: EnsureFn): void {
170
+ ensureImpl = fn;
171
+ }
172
+ /** The registered `ensure()` — throws NAMED if authoring never initialized (impossible in any
173
+ * real bundle: `read` and `ensure` ride the same library). Library-internal. */
174
+ export function __ensure(agg: unknown, id: string): { set(field: string, value: unknown): unknown } {
175
+ if (ensureImpl === null) {
176
+ throw new Error("attested read: the authoring surface is not initialized (no ensure())");
177
+ }
178
+ return ensureImpl(agg, id);
179
+ }
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));
@@ -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] }
@@ -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) => ((d as { declaredQueryReads?: unknown[] }).declaredQueryReads ?? []).length > 0,
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).
@@ -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
  /**
@@ -0,0 +1,185 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
4
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /**
7
+ * framework/attested_receipts.ts — ATTESTATION RECEIPTS + EQUIVOCATION ACCOUNTABILITY
8
+ * (attested reads slice B, stage 3 — `architecture/attested_reads_design.md` §5).
9
+ *
10
+ * The SOURCE's ledger must REMEMBER what it vouched: every attested-read signature the
11
+ * read-attestor emits is leaf-accumulated (`sha256hex(attested_read_signed_bytes)` — derived,
12
+ * never carried) and SEALED into the source's own chain as one {@link NomosAttestationReceipt}
13
+ * per batch — one write covers up to 1024 reads. The seal is an ORDINARY OFFER
14
+ * ({@link recordAttestationBatch}, authored AS the enrolled `workspace:read-attestor` through
15
+ * the one gate); the kernel's `attestation_ledger_violation` arm DEMANDS the receipt's
16
+ * `batchRoot` be the Merkle root of its carried leaves on every door, so a chain-committed
17
+ * receipt is always internally consistent. The adapter only SCHEDULES the seal (the debounced-
18
+ * admission-alarm precedent: scheduling affects WHEN, never WHAT or WHETHER).
19
+ *
20
+ * The READER's half: {@link reportSourceEquivocation} folds a {@link NomosSourceDishonor} fact
21
+ * from two mutually-incriminating signed envelopes (same source key, same fold_root, same
22
+ * (queryId, args), DIVERGENT canonical results). The kernel verifies the evidence — pure
23
+ * carried bytes, the xwi posture — at author, edge admission, AND verify_chain; a forged or
24
+ * non-contradictory report is a typed refusal (`EquivocationEvidenceInvalid`). Once folded,
25
+ * a trust edge declaring `onDishonor: "refuse"` refuses every LATER attestation from that
26
+ * `(genesis, keyHash)` — position-aware like revocation, never retroactive.
27
+ */
28
+ import { z } from "zod";
29
+ import { aggregate, instance } from "../aggregate.js";
30
+ import { t } from "../fields.js";
31
+ import { Lww } from "../drivers.js";
32
+ import { directive } from "../directive.js";
33
+ import { set } from "../ops.js";
34
+ import { query } from "../query.js";
35
+
36
+ /** The framework-reserved receipt instance-id prefix (kernel mirror:
37
+ * `nomos_executor::ATTESTATION_RECEIPT_PREFIX`). */
38
+ export const ATTESTATION_RECEIPT_PREFIX = "attest-receipt:";
39
+
40
+ /** The receipt instance id for a batch. */
41
+ export function attestationReceiptId(batchId: string): string {
42
+ return ATTESTATION_RECEIPT_PREFIX + batchId;
43
+ }
44
+
45
+ /** The framework-reserved dishonor instance-id prefix (kernel mirror:
46
+ * `nomos_executor::SOURCE_DISHONOR_PREFIX`). */
47
+ export const SOURCE_DISHONOR_PREFIX = "source-dishonor:";
48
+
49
+ /** The dishonor instance id for a proven `(sourceGenesis, keyHash)`. */
50
+ export function sourceDishonorId(sourceGenesis: string, keyHash: string): string {
51
+ return `${SOURCE_DISHONOR_PREFIX}${sourceGenesis}:${keyHash}`;
52
+ }
53
+
54
+ /**
55
+ * NomosAttestationReceipt (§5.1) — ONE sealed batch of attested-read receipt leaves in the
56
+ * SOURCE's own chain. Each leaf = `sha256hex(attested_read_signed_bytes)` — it commits to the
57
+ * FULL attestation (query, args, canonical result, head, fold_root, epoch, time) with zero
58
+ * extra signed bytes and NO reader identity (least-disclosure). `receiptPath` (the kernel
59
+ * query op) serves Merkle inclusion `{batchRoot, index, path}` from the folded `leaves`.
60
+ */
61
+ export const NomosAttestationReceipt = aggregate("NomosAttestationReceipt", {
62
+ /** `"<epoch>-<nSealedReceiptsInFold>-<keyHash8>"` — chain-derived mint (collision-free across remounts); promised on every envelope's `receipt_hint`. */
63
+ batchId: t.string().merge(Lww),
64
+ /** sha256 Merkle root over the batch's leaf hashes (kernel-verified == root(leaves) at the gate). */
65
+ batchRoot: t.string().merge(Lww),
66
+ /** The leaf count (kernel-verified == leaves.length at the gate). */
67
+ leafCount: t.int().merge(Lww),
68
+ /** The leaf hashes, in serve order (JSON array) — what `receiptPath` serves from. */
69
+ leaves: t.json().merge(Lww),
70
+ /** The `attested_at_ms` range the batch covers. */
71
+ firstAt: t.int().merge(Lww),
72
+ lastAt: t.int().merge(Lww),
73
+ /** The source chain position when the batch sealed. */
74
+ headAtSeal: t.string().merge(Lww),
75
+ foldRootAtSeal: t.string().merge(Lww),
76
+ /** Which read-attestor key signed every member of this batch (`sha256hex(pubkey)`). */
77
+ signerKeyHash: t.string().merge(Lww),
78
+ });
79
+
80
+ /** Audit read: the source's sealed receipt stream, by batch id. */
81
+ export const attestationReceiptByBatch = query("attestationReceiptByBatch")
82
+ .key("batchId")
83
+ .returns(NomosAttestationReceipt);
84
+
85
+ /**
86
+ * `recordAttestationBatch` (§5.2) — THE SEAL. One authored intent per batch, authored AS the
87
+ * enrolled `workspace:read-attestor` through the NORMAL offer gate (the key is an enrolled
88
+ * signer precisely so it can do this — §1.2's "one key, two duties"). The payload is the
89
+ * kernel's own `attestationOpenBatch` snapshot, carried verbatim by the adapter; the gate
90
+ * re-derives the Merkle root and refuses any drift (`AttestationReceiptInvalid`) — the
91
+ * adapter carries, never computes. Seal triggers (leafCount ≥ 1024 / age ≥ 60s / epoch change
92
+ * / unmount) are the kernel's verdict on the same snapshot; a failed seal retries, never
93
+ * drops silently.
94
+ */
95
+ export const recordAttestationBatch = directive("recordAttestationBatch")
96
+ .ensures(NomosAttestationReceipt)
97
+ .payload(
98
+ z.object({
99
+ batchId: z.string().min(1),
100
+ batchRoot: z.string().min(1),
101
+ leafCount: z.number().int().positive(),
102
+ leaves: z.array(z.string().min(1)).min(1),
103
+ firstAt: z.number().int().nonnegative(),
104
+ lastAt: z.number().int().nonnegative(),
105
+ headAtSeal: z.string(),
106
+ foldRootAtSeal: z.string(),
107
+ signerKeyHash: z.string().min(1),
108
+ }),
109
+ )
110
+ .plan((p) => {
111
+ const r = instance(NomosAttestationReceipt, attestationReceiptId(p.batchId));
112
+ return [
113
+ set(r, "batchId", p.batchId),
114
+ set(r, "batchRoot", p.batchRoot),
115
+ set(r, "leafCount", p.leafCount),
116
+ set(r, "leaves", JSON.stringify(p.leaves)),
117
+ set(r, "firstAt", p.firstAt),
118
+ set(r, "lastAt", p.lastAt),
119
+ set(r, "headAtSeal", p.headAtSeal),
120
+ set(r, "foldRootAtSeal", p.foldRootAtSeal),
121
+ set(r, "signerKeyHash", p.signerKeyHash),
122
+ ];
123
+ });
124
+
125
+ /**
126
+ * NomosSourceDishonor (§5.3) — a PROVEN equivocation, folded in the READER's (or a
127
+ * governor's) chain at `source-dishonor:<sourceGenesis>:<keyHash>`. The full evidence (the
128
+ * two signed envelopes) rides the committed intent's payload — the fact aggregate carries
129
+ * the identity tuple + when it was proven. Once folded, `onDishonor: "refuse"` trust edges
130
+ * refuse every LATER attestation from this `(genesis, keyHash)` (position-aware).
131
+ */
132
+ export const NomosSourceDishonor = aggregate("NomosSourceDishonor", {
133
+ /** The equivocating source chain's genesis oid (its true identity). */
134
+ sourceGenesis: t.string().merge(Lww),
135
+ /** The equivocating read-attestor key (`sha256hex(pubkey)`). */
136
+ keyHash: t.string().merge(Lww),
137
+ /** The fold-root BOTH envelopes pinned — the state the source spoke of with two tongues. */
138
+ foldRoot: t.string().merge(Lww),
139
+ /** When the reporter proved it (ms — the reporter's clock, informational). */
140
+ provenAtMs: t.int().merge(Lww),
141
+ });
142
+
143
+ /** Audit read: the folded dishonor facts, by source genesis. */
144
+ export const sourceDishonorByGenesis = query("sourceDishonorByGenesis")
145
+ .key("sourceGenesis")
146
+ .returns(NomosSourceDishonor);
147
+
148
+ /** One carried evidence envelope (an attested read exactly as captured — §5.3 walk 1). */
149
+ const evidenceEnvelope = z.object({
150
+ queryId: z.string().min(1),
151
+ args: z.record(z.string(), z.string()).default({}),
152
+ result: z.unknown(),
153
+ attestation: z.record(z.string(), z.unknown()),
154
+ });
155
+
156
+ /**
157
+ * `reportSourceEquivocation` (§5.3) — carry TWO signed attested-read envelopes; the KERNEL
158
+ * gate verifies (pure carried bytes): both ed25519 signatures, same source key, same
159
+ * `(source_genesis, fold_root, queryId, canonical args)`, DIVERGENT canonical results — and
160
+ * that the folded fact's id names exactly the proven `(genesis, keyHash)`. Forged or
161
+ * non-contradictory evidence is `EquivocationEvidenceInvalid` on every door. The plan trusts
162
+ * the payload's identity mirrors ONLY because the gate re-derives them from the evidence.
163
+ */
164
+ export const reportSourceEquivocation = directive("reportSourceEquivocation")
165
+ .ensures(NomosSourceDishonor)
166
+ .payload(
167
+ z.object({
168
+ a: evidenceEnvelope,
169
+ b: evidenceEnvelope,
170
+ /** Mirror of the proven identity (kernel-checked against the verified evidence). */
171
+ sourceGenesis: z.string().min(1),
172
+ keyHash: z.string().min(1),
173
+ foldRoot: z.string().min(1),
174
+ provenAtMs: z.number().int().nonnegative(),
175
+ }),
176
+ )
177
+ .plan((p) => {
178
+ const d = instance(NomosSourceDishonor, sourceDishonorId(p.sourceGenesis, p.keyHash));
179
+ return [
180
+ set(d, "sourceGenesis", p.sourceGenesis),
181
+ set(d, "keyHash", p.keyHash),
182
+ set(d, "foldRoot", p.foldRoot),
183
+ set(d, "provenAtMs", p.provenAtMs),
184
+ ];
185
+ });
@@ -0,0 +1,96 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
3
+ // wasm32-wasip1 artifact {kernel · projection · embedded QuickJS engine}.
4
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
5
+
6
+ /**
7
+ * framework/source_roster.ts — K3 TRACKED ROSTERS (attested reads slice B, stage 5 —
8
+ * `architecture/attested_reads_design.md` §1.5).
9
+ *
10
+ * The reader's chain maintains a {@link NomosSourceRoster} per trusted source: a reader-side
11
+ * mirror of the SOURCE's own read-attestor signer roster, delivered as ATTESTED READS of the
12
+ * source's signer chain itself (the kernel-native `attestorRoster` query — the roster rides
13
+ * the SAME attested-read machinery as any foreign premise). `roster: "tracked"` on the trust
14
+ * edge activates the folded roster inside `accepted_keys`; rotation then needs ZERO reader
15
+ * law deploys, and a source-side revocation propagates on the reader's next sync.
16
+ *
17
+ * THE GROUNDED RECURSION (§1.5.2): a roster read's envelope is verified against the accepted
18
+ * set AS ALREADY FOLDED in the reader's prefix — the FIRST sync therefore verifies against
19
+ * the bootstrap trust (the K1 pins or the K2 birth-cert-derived key), which exists by
20
+ * construction before any roster does; every later sync verifies against the roster its own
21
+ * prefix folded. Deterministic at replay: the roster is reader-chain state.
22
+ */
23
+ import { z } from "zod";
24
+ import { aggregate, instance } from "../aggregate.js";
25
+ import { t } from "../fields.js";
26
+ import { Lww } from "../drivers.js";
27
+ import { directive, type Directive } from "../directive.js";
28
+ import { set } from "../ops.js";
29
+ import { read } from "../read.js";
30
+ import { foreignQuery } from "../attested_core.js";
31
+
32
+ /** The framework-reserved roster instance-id prefix (kernel mirror:
33
+ * `nomos_executor::SOURCE_ROSTER_PREFIX`). */
34
+ export const SOURCE_ROSTER_PREFIX = "source-roster:";
35
+
36
+ /** The roster instance id for a source genesis. */
37
+ export function sourceRosterId(sourceGenesis: string): string {
38
+ return SOURCE_ROSTER_PREFIX + sourceGenesis;
39
+ }
40
+
41
+ /** The kernel-native source-side roster query id (`nomos_executor::ATTESTOR_ROSTER_QUERY`). */
42
+ export const ATTESTOR_ROSTER_QUERY_ID = "attestorRoster";
43
+
44
+ /**
45
+ * NomosSourceRoster (§1.5) — the reader's folded mirror of one source's read-attestor
46
+ * roster, keyed by the source's TRUE identity (`source-roster:<genesis>`). `rosterJson` is
47
+ * the `keyHash → leaf` map exactly as the source's signer chain folds it (a key is ACCEPTED
48
+ * iff its leaf's `status == "active"` — the position-aware revocation mirror); the kernel's
49
+ * `accepted_keys` reads THIS under `roster: "tracked"`.
50
+ */
51
+ export const NomosSourceRoster = aggregate("NomosSourceRoster", {
52
+ /** The routable source name the edge was declared under (diagnostics). */
53
+ source: t.string().merge(Lww),
54
+ /** The `keyHash → enrollment leaf` map, JSON — as attested by the source's own chain. */
55
+ rosterJson: t.json().merge(Lww),
56
+ /** The source's `SignerEpoch` at the attested sync. */
57
+ currentEpoch: t.int().merge(Lww),
58
+ });
59
+
60
+ /** One roster row as the source's `attestorRoster` query answers it. */
61
+ interface AttestorRosterRow {
62
+ id: string;
63
+ data: {
64
+ genesis: string;
65
+ currentEpoch: number;
66
+ keys: Record<string, unknown>;
67
+ };
68
+ }
69
+
70
+ /**
71
+ * BUILD the reader-side `syncSourceRoster` framework directive for ONE tracked source
72
+ * (§1.5.1) — export the result from the law that declares `trustSource(sourceWs, { roster:
73
+ * "tracked", … })`. Its plan performs `read(attestorRoster, {}, { from: sourceWs })` (an
74
+ * ordinary attested read — verified by the five-step arm against the accepted set the reader's
75
+ * PREFIX already folds: pins/birth-cert first, then the roster itself) and folds the answer
76
+ * into {@link NomosSourceRoster}. One directive per tracked source per law (the id embeds
77
+ * nothing — a law tracking several sources exports several via distinct ids).
78
+ */
79
+ export function sourceRosterSync(sourceWs: string, directiveId = "syncSourceRoster"): Directive<Record<string, never>> {
80
+ const rosterQuery = foreignQuery(ATTESTOR_ROSTER_QUERY_ID);
81
+ return directive(directiveId)
82
+ .ensures(NomosSourceRoster)
83
+ .payload(z.object({}).strict())
84
+ .plan(() => {
85
+ const rows = read<AttestorRosterRow[]>(rosterQuery, {}, { from: sourceWs });
86
+ if (rows.length === 0) throw new Error("attestor-roster-empty");
87
+ const d = rows[0]!.data;
88
+ const r = instance(NomosSourceRoster, sourceRosterId(d.genesis));
89
+ return [
90
+ set(r, "source", sourceWs),
91
+ set(r, "rosterJson", JSON.stringify(d.keys ?? {})),
92
+ set(r, "currentEpoch", d.currentEpoch ?? 0),
93
+ ];
94
+ })
95
+ .readsFrom(sourceWs, rosterQuery) as Directive<Record<string, never>>;
96
+ }
package/src/index.ts CHANGED
@@ -120,6 +120,46 @@ 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
+ trustParent,
128
+ PARENT_TRUST_SENTINEL,
129
+ foreignQuery,
130
+ NomosSourceFrontier,
131
+ sourceFrontierId,
132
+ SOURCE_FRONTIER_PREFIX,
133
+ nomosSourceFrontierEnsure,
134
+ type TrustSourceDecl,
135
+ type TrustSourceOptions,
136
+ type ForeignQueryRef,
137
+ } from "./attested.js";
138
+ // ATTESTATION RECEIPTS + EQUIVOCATION ACCOUNTABILITY (slice B stage 3 — §5): the source-side
139
+ // sealed receipt batch + the reader-side proven-dishonor fact, both ordinary law the kernel's
140
+ // `attestation_ledger_violation` arm guards on every door.
141
+ export {
142
+ NomosAttestationReceipt,
143
+ recordAttestationBatch,
144
+ attestationReceiptByBatch,
145
+ attestationReceiptId,
146
+ ATTESTATION_RECEIPT_PREFIX,
147
+ NomosSourceDishonor,
148
+ reportSourceEquivocation,
149
+ sourceDishonorByGenesis,
150
+ sourceDishonorId,
151
+ SOURCE_DISHONOR_PREFIX,
152
+ } from "./framework/attested_receipts.js";
153
+ // K3 TRACKED ROSTERS (slice B stage 5 — §1.5): the reader-side folded source roster + the
154
+ // per-source sync directive factory (roster updates ride the attested-read machinery itself,
155
+ // grounded on the pins/birth-cert bootstrap).
156
+ export {
157
+ NomosSourceRoster,
158
+ sourceRosterSync,
159
+ sourceRosterId,
160
+ SOURCE_ROSTER_PREFIX,
161
+ ATTESTOR_ROSTER_QUERY_ID,
162
+ } from "./framework/source_roster.js";
123
163
  // THE BIRTH EFFECT (G3 — birth is a declared kernel primitive): a `.births()` directive's plan records the
124
164
  // child's genesis recipe via `birth(recipe)` (reuse `buildGenesisChain`); it rides the committed intent as a
125
165
  // 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>(query: QueryDecl, args: Record<string, string>): T {
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 result = host().read(query.id, args) as T;
73
- footprint.push({ queryId: query.id, args, result });
74
- return result;
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
- directives: manifest.directives.map((d) => (namespaced.has(d.target) ? { ...d, target: qa(d.target) } : d)),
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)) }
@@ -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
  /**