@githolon/dsl 0.34.0 → 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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.34.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",
@@ -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));
@@ -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] }
@@ -1835,6 +1835,13 @@ function emitDirective(
1835
1835
  const ctorParams = specs
1836
1836
  .map((s) => ` ${s.optional ? "" : "required "}this.${s.name},`)
1837
1837
  .join("\n");
1838
+ // Zero-field payload (e.g. a `.births()` sanctioning directive with an empty
1839
+ // object schema): `const X({});` / a dangling blank field block is INVALID Dart —
1840
+ // emit a plain `const X();` and an empty map literal instead.
1841
+ const hasFields = specs.length > 0;
1842
+ const ctorLines = hasFields
1843
+ ? [` const ${className}({`, ctorParams, ` });`]
1844
+ : [` const ${className}();`];
1838
1845
 
1839
1846
  // toPayloadJson: serialise only typed fields to data. No Event, no Hlc; the
1840
1847
  // GitHolon executes the installed domain plan. Optional
@@ -1870,12 +1877,9 @@ function emitDirective(
1870
1877
  ` /// The referential marker (creates/mutates/ensures/archives).`,
1871
1878
  ` static const String marker = '${d.marker}';`,
1872
1879
  ``,
1873
- fieldDecls,
1874
- ``,
1880
+ ...(hasFields ? [fieldDecls, ``] : []),
1875
1881
  ` /// ${docFirst}`,
1876
- ` const ${className}({`,
1877
- ctorParams,
1878
- ` });`,
1882
+ ...ctorLines,
1879
1883
  ``,
1880
1884
  ` @override`,
1881
1885
  ` String get domain => domainName;`,
@@ -2785,6 +2789,14 @@ export interface DomainModule {
2785
2789
  parentAttestedBirth?:
2786
2790
  | boolean
2787
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[];
2788
2800
  /**
2789
2801
  * The domain's NATIVE ZANZIBAR RELATION SCHEMA (`relation_schema.ts` — the userset-rewrite law,
2790
2802
  * P1). Pure-data per-type relation rewrites lowered to the `nomosRelations` USD era key (NO JS on
@@ -1766,9 +1766,10 @@ function v2IdentityAndCleanupPreamble(opts: TsProofOptions, hashConst: string, n
1766
1766
  }
1767
1767
 
1768
1768
  /**
1769
- * THE V2 CATCH/FINALLY (G1, gap 4): retire the throwaway workspace on every exit via the OPEN lane (no
1770
- * Bearer — there is no host secret to present; a retire is a SIGNED governance offer in the real lane, but
1771
- * the open self-retire suffices to clean up a throwaway). PROOF_KEEP=1 keeps it.
1769
+ * THE V2 CATCH/FINALLY (G1, gap 4 · 0.34.1): retirement is a SIGNED governance offer — the open self-retire
1770
+ * lane is DEAD post-warrant-flip, so the proof itself never retires. Under `githolon proof --live`
1771
+ * (NOMOS_PROOF_MANAGED=1) the CLI created the workspace REGISTERED and retires it lawfully after exit;
1772
+ * standalone, the proof warns LOUDLY and names the manual command. PROOF_KEEP=1 keeps it.
1772
1773
  */
1773
1774
  function v2CatchFinally(): string[] {
1774
1775
  return [
@@ -1776,13 +1777,18 @@ function v2CatchFinally(): string[] {
1776
1777
  ` runError = e;`,
1777
1778
  ` console.error("✗ " + ((e as Error)?.message ?? e));`,
1778
1779
  `} finally {`,
1779
- ` // THE CLEANUP CONTRACT: never an orphaned cloud workspace. Retired on every exit path (open lane — no`,
1780
- ` // host secret exists post-G1); PROOF_KEEP=1 keeps it and prints the persist command.`,
1780
+ ` // THE CLEANUP CONTRACT: never a SILENTLY orphaned cloud workspace. Retirement is a SIGNED`,
1781
+ ` // governance offer (retireOwnWorkspace at the parent) — the old open-lane POST /retire is dead`,
1782
+ ` // post-warrant-flip. \`githolon proof --live\` orchestrates it (NOMOS_PROOF_MANAGED=1: it created`,
1783
+ ` // the workspace REGISTERED and retires it after this process exits). Run standalone, this proof`,
1784
+ ` // cannot lawfully retire (no signing key) — so it says so LOUDLY and names the exact command.`,
1781
1785
  ` if (born_ws && process.env.PROOF_KEEP === "1") {`,
1782
1786
  ` console.log(\`workspace \${WS} KEPT — retire it when done: githolon ws retire \${WS}\`);`,
1787
+ ` } else if (born_ws && process.env.NOMOS_PROOF_MANAGED === "1") {`,
1788
+ ` console.log(\` workspace \${WS} — retirement handled by githolon proof --live (signed retireOwnWorkspace)\`);`,
1783
1789
  ` } else if (born_ws) {`,
1784
- ` const rr = await fetch(\`\${CLOUD}/v2/workspaces/\${WS}/retire\`, { method: "POST", headers: { "x-nomos-principal": PRINCIPAL } }).then((x) => x.json()).catch(() => ({}));`,
1785
- ` console.log(\` retired \${WS}: \${(rr as { ok?: boolean }).ok === true}\`);`,
1790
+ ` console.error(\`⚠ THROWAWAY WORKSPACE NOT RETIRED: \${WS} (retirement is a signed governance offer this standalone proof cannot author)\`);`,
1791
+ ` console.error(\`⚠ retire it manually: githolon ws retire \${WS} (run the managed lane next time: githolon proof --live)\`);`,
1786
1792
  ` }`,
1787
1793
  ` process.exit(runError ? 1 : 0);`,
1788
1794
  `}`,
@@ -356,8 +356,19 @@ const DART_SUPPORT_SRC = [
356
356
  // The async WebView bridge runtime (NomosBridge / BridgeTransport) the generated
357
357
  // `<name>.client.dart` drives — the barrel re-exports it, so it must be vendored too.
358
358
  "webview_bridge.dart",
359
+ // Typed diagnostics (nomosErrorFromInfo / MissingOfferAuthority / …) — the vendored
360
+ // bridge AND the generated `<name>.client.dart` reference these through the barrel,
361
+ // so skipping it left `dart analyze` red on every vendored output (0.34.1). No
362
+ // external pub deps.
363
+ "errors.dart",
359
364
  ] as const;
360
365
 
366
+ /** Barrel exports DROPPED in vendored mode: `schema_validation.dart` (drags the external
367
+ * `json_schema` pub dep) and `home.dart` (the home-onboarding runtime — drags
368
+ * `package:crypto`; nothing the codegen emits references it). Package-import mode
369
+ * (a real `nomos_client` pub dep) keeps the full barrel. */
370
+ const VENDORED_BARREL_EXCLUDES = ["src/schema_validation.dart", "src/home.dart"] as const;
371
+
361
372
  function removeVendoredDartSupport(dartOut: string): void {
362
373
  rmSync(path.join(dartOut, "nomos_client.dart"), { force: true });
363
374
  rmSync(path.join(dartOut, "src"), { recursive: true, force: true });
@@ -385,7 +396,7 @@ function emitDart(
385
396
  const libDir = path.join(DSL_DIR, "dart", "lib");
386
397
  const barrel = readFileSync(path.join(libDir, "nomos_client.dart"), "utf8")
387
398
  .split("\n")
388
- .filter((line) => !line.includes("src/schema_validation.dart"))
399
+ .filter((line) => !VENDORED_BARREL_EXCLUDES.some((x) => line.includes(x)))
389
400
  .join("\n");
390
401
  writeFileSync(path.join(dartOut, "nomos_client.dart"), barrel, "utf8");
391
402
  for (const f of DART_SUPPORT_SRC) {
@@ -1261,9 +1272,14 @@ async function main(): Promise<void> {
1261
1272
  // `read()`'s body (dsl/src/read.ts — keep them byte-identical) and is tree-shaken
1262
1273
  // out of read-free lumps.
1263
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.
1264
1278
  const declaresCapturedReads = domainModules.some((m) =>
1265
1279
  m.directives.some(
1266
- (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,
1267
1283
  ),
1268
1284
  );
1269
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
  /**
@@ -850,12 +850,17 @@ export const retireOwnOnly = workspaceInvariant("RetireOwnOnly")
850
850
  ])
851
851
  .assert((refs, ctx) => {
852
852
  const target = (refs as { target?: { principal?: string; createdBy?: string } }).target;
853
- const author = ctx?.principal ?? "";
853
+ // The captured, VERIFIED author rides as a SUBJECT (`user:<uid>`); the record's creator fields
854
+ // (`principal`/`createdBy`, stamped at createWorkspace/birth) are the BARE uid — normalize the
855
+ // prefix before comparing (0.34.1: the raw compare could never match, so retireOwnWorkspace
856
+ // refused its own creator).
857
+ const bare = (s: string): string => (s.startsWith("user:") ? s.slice(5) : s);
858
+ const author = bare(ctx?.principal ?? "");
854
859
  // The captured, VERIFIED author must equal the record's recorded creator (its `principal`/`createdBy`,
855
860
  // set by the same identity at createWorkspace). So a creator may retire ONLY its own workspaces.
856
861
  return author &&
857
- ((target?.principal && target.principal === author) ||
858
- (target?.createdBy && target.createdBy === author))
862
+ ((target?.principal && bare(target.principal) === author) ||
863
+ (target?.createdBy && bare(target.createdBy) === author))
859
864
  ? { accept: true }
860
865
  : { reject: "retire-own-only: only the workspace's creator may retire it" };
861
866
  });
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>(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
  /**