@githolon/dsl 0.15.0 → 0.16.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dart/CHANGELOG.md +2 -0
- package/dart/FINDINGS.md +2 -2
- package/dart/LICENSE +45 -0
- package/dart/README.md +9 -0
- package/dart/example/nomos_webview_transport.dart +4 -4
- package/dart/lib/src/offer.dart +2 -2
- package/dart/lib/src/subscriptions.dart +1 -1
- package/dart/lib/src/webview_bridge.dart +52 -0
- package/dart/pubspec.yaml +3 -3
- package/dart/test/schema_validation_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +1 -1
- package/dart/test/wire_test.dart +1 -1
- package/package.json +1 -1
- package/src/aggregate.ts +9 -1
- package/src/authoring.ts +10 -1
- package/src/codegen_dart.ts +25 -15
- package/src/codegen_dart_client.ts +54 -5
- package/src/codegen_openapi.ts +239 -0
- package/src/codegen_proof.ts +16 -9
- package/src/codegen_ts.ts +83 -22
- package/src/compile_package_main.ts +117 -29
- package/src/ctx.ts +13 -0
- package/src/directive.ts +1 -1
- package/src/engine_entry.ts +22 -5
- package/src/fields.ts +44 -0
- package/src/flow_gate.ts +208 -0
- package/src/framework/capability.ts +19 -1
- package/src/framework/encryption_scope.ts +9 -1
- package/src/framework/genesis.ts +5 -1
- package/src/framework/relations.ts +23 -4
- package/src/framework/repair.ts +2 -2
- package/src/framework/signing_identity.ts +1 -1
- package/src/framework/workspace_invariant.ts +29 -8
- package/src/framework/workspaces.ts +147 -15
- package/src/index.ts +24 -0
- package/src/manifest.ts +12 -18
- package/src/ops.ts +42 -1
- package/src/principal_evidence.ts +112 -0
- package/src/relation_sugar.ts +10 -4
- package/src/shape_compat.ts +208 -0
- package/src/usd.ts +131 -0
- package/src/usd_layers.ts +43 -0
- package/src/wire.ts +8 -0
- package/src/wire_encode.ts +75 -2
- package/src/workspace_sharding.ts +196 -18
- package/src/workspace_type.ts +3 -2
- /package/dart/lib/{nomos_dsl.dart → nomos_client.dart} +0 -0
|
@@ -0,0 +1,208 @@
|
|
|
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. If a file isn't this / hosting this / authoring for this
|
|
4
|
+
// / proving this — it's gone.
|
|
5
|
+
//
|
|
6
|
+
// SHAPE COMPATIBILITY (#72) — the structural contract a client and a workspace
|
|
7
|
+
// negotiate by, INSTEAD of the package hash. See architecture/shape_compatibility.md.
|
|
8
|
+
//
|
|
9
|
+
// A client fits a workspace iff the workspace's installed interface SUBTYPES what the
|
|
10
|
+
// client depends on:
|
|
11
|
+
// • WRITES are CONTRAVARIANT — for every directive the client calls, the workspace
|
|
12
|
+
// has that directive id and its CURRENT payload schema ACCEPTS the client's
|
|
13
|
+
// payload: every field the workspace REQUIRES the client always sends, and every
|
|
14
|
+
// shared field's kind matches. (Extra client fields are ignored — non-strict zod.)
|
|
15
|
+
// • READS are COVARIANT by STABLE ID (#58) — for every aggregate the client reads,
|
|
16
|
+
// the workspace has that aggregate SID and every field the client CONSUMES (by
|
|
17
|
+
// field sid) is present with a compatible kind. The workspace may serve MORE.
|
|
18
|
+
//
|
|
19
|
+
// Identity is by stable id, not name or hash — that is what makes the relation
|
|
20
|
+
// decidable across renames (a renamed aggregate field keeps its sid, so an old client
|
|
21
|
+
// stays read-compatible). Directive/payload identity is the dispatched id + field
|
|
22
|
+
// names (directives are not sid-tracked yet — a payload rename IS a write break, and
|
|
23
|
+
// is reported precisely).
|
|
24
|
+
|
|
25
|
+
import { z } from "zod";
|
|
26
|
+
import { introspectPayload, type DomainModule, type PayloadFieldSpec } from "./codegen_dart.js";
|
|
27
|
+
import type { StableIds } from "./stable_ids_types.js";
|
|
28
|
+
import type { Field } from "./fields.js";
|
|
29
|
+
|
|
30
|
+
/** One payload field's normalized shape: its kind + whether the schema makes it optional. */
|
|
31
|
+
export interface PayloadFieldShape {
|
|
32
|
+
readonly kind: string;
|
|
33
|
+
readonly optional: boolean;
|
|
34
|
+
}
|
|
35
|
+
/** A directive's payload contract — the field set + types the gate validates against. */
|
|
36
|
+
export interface DirectivePayloadShape {
|
|
37
|
+
readonly fields: Record<string, PayloadFieldShape>;
|
|
38
|
+
}
|
|
39
|
+
/** An aggregate's read contract — fields keyed by STABLE ID (rename-safe), each carrying
|
|
40
|
+
* its current label (for human-readable verdicts) + its read kind. */
|
|
41
|
+
export interface AggregateReadShape {
|
|
42
|
+
readonly sid: string;
|
|
43
|
+
readonly label: string;
|
|
44
|
+
/** keyed by FIELD SID. */
|
|
45
|
+
readonly fields: Record<string, { kind: string; label: string }>;
|
|
46
|
+
}
|
|
47
|
+
/** A domain's full structural interface — the offer (a workspace's installed law) AND
|
|
48
|
+
* the requirement (the law a client was generated from) are the SAME vocabulary. */
|
|
49
|
+
export interface DomainInterface {
|
|
50
|
+
readonly v: 1;
|
|
51
|
+
readonly domain: string;
|
|
52
|
+
readonly domainSid?: string;
|
|
53
|
+
/** keyed by DIRECTIVE ID (the dispatched name). */
|
|
54
|
+
readonly directives: Record<string, DirectivePayloadShape>;
|
|
55
|
+
/** keyed by AGGREGATE SID. */
|
|
56
|
+
readonly aggregates: Record<string, AggregateReadShape>;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The verdict: compatible iff nothing is missing and nothing is type-incompatible.
|
|
60
|
+
* `missing` = a depended-on directive/aggregate/field the workspace doesn't expose;
|
|
61
|
+
* `incompatible` = present but type-divergent (a payload field kind, a retyped read field). */
|
|
62
|
+
export interface CompatVerdict {
|
|
63
|
+
readonly ok: boolean;
|
|
64
|
+
readonly missing: string[];
|
|
65
|
+
readonly incompatible: string[];
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/** Normalize a directive payload field to a language-neutral kind from its introspected
|
|
69
|
+
* spec (the SAME `introspectPayload` the Dart/TS clients are generated from, so both
|
|
70
|
+
* sides of a comparison speak one vocabulary). */
|
|
71
|
+
function payloadFieldKind(spec: PayloadFieldSpec): string {
|
|
72
|
+
switch (spec.toJson) {
|
|
73
|
+
case "enum":
|
|
74
|
+
return "enum";
|
|
75
|
+
case "strList":
|
|
76
|
+
return "set";
|
|
77
|
+
case "json":
|
|
78
|
+
return "json";
|
|
79
|
+
case "scalar":
|
|
80
|
+
default:
|
|
81
|
+
switch (spec.dartType) {
|
|
82
|
+
case "int":
|
|
83
|
+
return "int";
|
|
84
|
+
case "double":
|
|
85
|
+
return "double";
|
|
86
|
+
case "bool":
|
|
87
|
+
return "bool";
|
|
88
|
+
default:
|
|
89
|
+
return "string";
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/** The aggregate read kind when no stable-id kind is recorded (pre-#58 fallback): the
|
|
95
|
+
* field's own declared kind, with the read-collapsed CRDTs normalized to their scalar. */
|
|
96
|
+
function aggFieldReadKind(f: Field): string {
|
|
97
|
+
switch (f.kind) {
|
|
98
|
+
case "counter":
|
|
99
|
+
return "int";
|
|
100
|
+
case "position":
|
|
101
|
+
return "string";
|
|
102
|
+
default:
|
|
103
|
+
return f.kind;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/** Extract a domain's structural interface from its loaded module + (optionally) its
|
|
108
|
+
* stable-id surface. Without stable ids (pre-#58) aggregate fields key by NAME and the
|
|
109
|
+
* kind comes from the field declaration — a graceful, rename-unaware fallback. */
|
|
110
|
+
export function domainInterface(mod: DomainModule, stableIds?: StableIds): DomainInterface {
|
|
111
|
+
const enumTypes = new Map<string, string>();
|
|
112
|
+
// DETERMINISTIC KEY ORDER — directive ids, aggregate sids, and field keys are SORTED, so the
|
|
113
|
+
// emitted interface is byte-stable and the layered-USD fold (which merges + sorts) reproduces it
|
|
114
|
+
// byte-identically (the `nomosStableIds`/`nomosReadGate` convention). architecture/shape_compatibility.md.
|
|
115
|
+
const sortedRec = <T>(entries: Array<[string, T]>): Record<string, T> => {
|
|
116
|
+
const out: Record<string, T> = {};
|
|
117
|
+
for (const [k, v] of entries.sort((a, b) => (a[0] < b[0] ? -1 : a[0] > b[0] ? 1 : 0))) out[k] = v;
|
|
118
|
+
return out;
|
|
119
|
+
};
|
|
120
|
+
const directives: Record<string, DirectivePayloadShape> = {};
|
|
121
|
+
const dirEntries: Array<[string, DirectivePayloadShape]> = [];
|
|
122
|
+
for (const d of (mod.directives ?? []) as Array<{ id?: unknown; payloadSchema?: unknown }>) {
|
|
123
|
+
if (!d || typeof d.id !== "string") continue;
|
|
124
|
+
const fieldEntries: Array<[string, PayloadFieldShape]> = [];
|
|
125
|
+
const schema = d.payloadSchema;
|
|
126
|
+
if (schema && typeof schema === "object") {
|
|
127
|
+
for (const spec of introspectPayload(schema as z.ZodTypeAny, enumTypes)) {
|
|
128
|
+
fieldEntries.push([spec.name, { kind: payloadFieldKind(spec), optional: !!spec.optional }]);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
dirEntries.push([d.id, { fields: sortedRec(fieldEntries) }]);
|
|
132
|
+
}
|
|
133
|
+
Object.assign(directives, sortedRec(dirEntries));
|
|
134
|
+
const aggregates: Record<string, AggregateReadShape> = {};
|
|
135
|
+
const aggEntries: Array<[string, AggregateReadShape]> = [];
|
|
136
|
+
for (const agg of (mod.aggregates ?? []) as Array<{ id: string; fields: Record<string, Field> }>) {
|
|
137
|
+
const label = agg.id;
|
|
138
|
+
const sa = stableIds?.aggregates?.[label];
|
|
139
|
+
const aggSid = sa?.sid ?? label;
|
|
140
|
+
const fieldEntries: Array<[string, { kind: string; label: string }]> = [];
|
|
141
|
+
for (const [fname, f] of Object.entries(agg.fields ?? {})) {
|
|
142
|
+
if (!f || f.kind === "hasMany") continue;
|
|
143
|
+
const fsid = sa?.fields?.[fname]?.sid ?? fname;
|
|
144
|
+
const kind = sa?.fields?.[fname]?.kind ?? aggFieldReadKind(f);
|
|
145
|
+
fieldEntries.push([fsid, { kind, label: fname }]);
|
|
146
|
+
}
|
|
147
|
+
aggEntries.push([aggSid, { sid: aggSid, label, fields: sortedRec(fieldEntries) }]);
|
|
148
|
+
}
|
|
149
|
+
Object.assign(aggregates, sortedRec(aggEntries));
|
|
150
|
+
return {
|
|
151
|
+
v: 1,
|
|
152
|
+
domain: (mod as { domain?: string; name?: string }).domain ?? (mod as { name?: string }).name ?? "",
|
|
153
|
+
...(stableIds?.domain !== undefined ? { domainSid: stableIds.domain } : {}),
|
|
154
|
+
directives,
|
|
155
|
+
aggregates,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Decide whether a CLIENT interface is satisfied by a WORKSPACE interface (the offer).
|
|
160
|
+
* Pure + deterministic — the SAME relation runs at connect() and (mirrored) at the
|
|
161
|
+
* editor/CI. Names the exact directive / aggregate / field at fault, never a hash. */
|
|
162
|
+
export function compatible(client: DomainInterface, workspace: DomainInterface): CompatVerdict {
|
|
163
|
+
const missing: string[] = [];
|
|
164
|
+
const incompatible: string[] = [];
|
|
165
|
+
|
|
166
|
+
// WRITES — contravariant. The workspace must accept every payload the client sends.
|
|
167
|
+
for (const [did, cShape] of Object.entries(client.directives)) {
|
|
168
|
+
const wShape = workspace.directives[did];
|
|
169
|
+
if (!wShape) {
|
|
170
|
+
missing.push(`directive '${did}'`);
|
|
171
|
+
continue;
|
|
172
|
+
}
|
|
173
|
+
// Every field the WORKSPACE requires, the client must ALWAYS send (present + required).
|
|
174
|
+
for (const [f, wf] of Object.entries(wShape.fields)) {
|
|
175
|
+
if (wf.optional) continue;
|
|
176
|
+
const cf = cShape.fields[f];
|
|
177
|
+
if (!cf || cf.optional) {
|
|
178
|
+
incompatible.push(`directive '${did}': workspace requires field '${f}' the client does not always send`);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
// Every field both sides know must agree on kind (a payload retype is a write break).
|
|
182
|
+
for (const [f, cf] of Object.entries(cShape.fields)) {
|
|
183
|
+
const wf = wShape.fields[f];
|
|
184
|
+
if (wf && wf.kind !== cf.kind) {
|
|
185
|
+
incompatible.push(`directive '${did}': field '${f}' kind ${cf.kind} ≠ workspace ${wf.kind}`);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
// READS — covariant by stable id. Every field the client consumes must be served compatibly.
|
|
191
|
+
for (const [sid, cAgg] of Object.entries(client.aggregates)) {
|
|
192
|
+
const wAgg = workspace.aggregates[sid];
|
|
193
|
+
if (!wAgg) {
|
|
194
|
+
missing.push(`aggregate '${cAgg.label}'`);
|
|
195
|
+
continue;
|
|
196
|
+
}
|
|
197
|
+
for (const [fsid, cf] of Object.entries(cAgg.fields)) {
|
|
198
|
+
const wf = wAgg.fields[fsid];
|
|
199
|
+
if (!wf) {
|
|
200
|
+
missing.push(`field '${cAgg.label}.${cf.label}'`);
|
|
201
|
+
} else if (wf.kind !== cf.kind) {
|
|
202
|
+
incompatible.push(`field '${cAgg.label}.${cf.label}': read kind ${cf.kind} ≠ workspace ${wf.kind}`);
|
|
203
|
+
}
|
|
204
|
+
}
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
return { ok: missing.length === 0 && incompatible.length === 0, missing, incompatible };
|
|
208
|
+
}
|
package/src/usd.ts
CHANGED
|
@@ -41,7 +41,9 @@ import {
|
|
|
41
41
|
} from "./compose.js";
|
|
42
42
|
import type { WireSchema, WireRefMode } from "./wire.js";
|
|
43
43
|
import type { DomainModule } from "./codegen_dart.js";
|
|
44
|
+
import { domainInterface } from "./shape_compat.js";
|
|
44
45
|
import { relationType, user, key, anyUser } from "./relation_schema.js";
|
|
46
|
+
import { harvestPrincipalFlows } from "./wire_encode.js";
|
|
45
47
|
|
|
46
48
|
/** The referential marker of a directive prim (kernel `RefMode` wire string). */
|
|
47
49
|
export type RefMode = WireRefMode;
|
|
@@ -292,6 +294,15 @@ export interface UsdLayer {
|
|
|
292
294
|
* whose old AND new packages both carry this key is DIFFED BY SID at the one gate.
|
|
293
295
|
*/
|
|
294
296
|
readonly nomosStableIds?: import("./stable_ids_types.js").StableIds;
|
|
297
|
+
/**
|
|
298
|
+
* THE STRUCTURAL INTERFACE (#72) — this domain's directives (payload shapes) + aggregates
|
|
299
|
+
* (read shapes, sid-keyed). ON-LEDGER (it rides the package, which installs as the
|
|
300
|
+
* `policy:{hash}.bundle`), so a client reads the workspace's OFFER straight from the folded
|
|
301
|
+
* law — reactively, host-blind (architecture/shape_compatibility.md). A client negotiates
|
|
302
|
+
* shape-compatibility against THIS, not the package hash. Pre-#72 bundles omit it; a client
|
|
303
|
+
* then reports `offerKnown:false` and never refuses on its absence (era-safe, graceful).
|
|
304
|
+
*/
|
|
305
|
+
readonly nomosInterface?: import("./shape_compat.js").DomainInterface;
|
|
295
306
|
/**
|
|
296
307
|
* THE CAPTURED-READ GATE DECLARATION (#58). Present IFF ≥1 directive of this layer
|
|
297
308
|
* declares a captured-read query (`.reads(someQuery)`): the law's EXPLICIT opt-in
|
|
@@ -341,6 +352,47 @@ export interface UsdLayer {
|
|
|
341
352
|
readonly v: 1;
|
|
342
353
|
readonly aggregates: Record<string, { scope: string; fields: string[]; delegatable: false }>;
|
|
343
354
|
};
|
|
355
|
+
/**
|
|
356
|
+
* THE CAPABILITY DECLARATION IN LAW (Full-A — capability_marketplace.md §4): the impure-
|
|
357
|
+
* capability quartets (obligation/complete/fail/block/deadLetter) lowered into the SEALED
|
|
358
|
+
* law, so the holon SURFACES them on the `declared_capabilities` read and the host relays a
|
|
359
|
+
* deploy-time "declared — bind it or obligations wait" notice without reaching into root
|
|
360
|
+
* law-state (boundness stays P6 — it is cross-holon root state). OMITTED for capability-free
|
|
361
|
+
* law (byte-identical hash — the era rule); declaring a capability MOVES the domainHash
|
|
362
|
+
* (named, expected). SORTED by aggregateId.
|
|
363
|
+
*/
|
|
364
|
+
readonly nomosCapabilities?: {
|
|
365
|
+
readonly v: 1;
|
|
366
|
+
readonly capabilities: readonly {
|
|
367
|
+
readonly capability: string;
|
|
368
|
+
readonly aggregateId: string;
|
|
369
|
+
readonly obligation: string;
|
|
370
|
+
readonly complete: string;
|
|
371
|
+
readonly fail: string;
|
|
372
|
+
readonly block: string;
|
|
373
|
+
readonly deadLetter: string;
|
|
374
|
+
readonly tasksQueryId: string | null;
|
|
375
|
+
/** The HTTPS endpoint the obligation is offered to (outbox); absent ⇒ no destination. */
|
|
376
|
+
readonly endpoint?: string;
|
|
377
|
+
}[];
|
|
378
|
+
};
|
|
379
|
+
/**
|
|
380
|
+
* THE FLOW GATE WAIVERS (architecture/security_model.md) — both halves of the audit surface,
|
|
381
|
+
* hashed into the law + replay-verified. `declassify` (confidentiality, rule 4): every acknowledged
|
|
382
|
+
* secret-correlated refusal. `endorse` (the integrity dual): every harvested PrincipalEvidence flow
|
|
383
|
+
* into an authority/standing/attribution sink — discovery IS law, witnessed fail-closed at runtime.
|
|
384
|
+
* Each half is OMITTED when empty (the era rule — a flow-free domain is byte-identical).
|
|
385
|
+
*/
|
|
386
|
+
readonly nomosFlowGate?: {
|
|
387
|
+
readonly v: 1;
|
|
388
|
+
readonly declassify?: readonly { readonly site: string; readonly reason: string }[];
|
|
389
|
+
readonly endorse?: readonly {
|
|
390
|
+
readonly directiveId: string;
|
|
391
|
+
readonly sink: string;
|
|
392
|
+
readonly facet: string;
|
|
393
|
+
readonly reason: string | null;
|
|
394
|
+
}[];
|
|
395
|
+
};
|
|
344
396
|
}
|
|
345
397
|
|
|
346
398
|
/** The reserved variant name declaring a set's DEFAULT (USD has a default variant). */
|
|
@@ -700,6 +752,70 @@ export function emitUsd(
|
|
|
700
752
|
encAggregates[a.id] = { scope: a.requiresRead, fields: [...a.encryptedFields], delegatable: false };
|
|
701
753
|
}
|
|
702
754
|
}
|
|
755
|
+
// THE CAPABILITY DECLARATION IN LAW (Full-A — capability_marketplace.md §4): the
|
|
756
|
+
// impure-capability quartets lowered into the SEALED law, so the holon can SURFACE
|
|
757
|
+
// them on a read (`declared_capabilities`) and the host RELAYS a deploy-time notice
|
|
758
|
+
// ("declares 'x' — bind it or obligations wait") WITHOUT reaching into root law-state.
|
|
759
|
+
// Mirrors the deploy-sidecar `capabilityLowerings` extraction; SORTED by aggregate id;
|
|
760
|
+
// omit-when-empty so capability-free law is byte-identical (the era rule). Declaring a
|
|
761
|
+
// capability now MOVES the domainHash (named, expected — like the stable-ids key).
|
|
762
|
+
const nomosCapabilities = [...((l.module as { impureCapabilities?: readonly unknown[] }).impureCapabilities ?? [])]
|
|
763
|
+
.map((c) => {
|
|
764
|
+
const k = c as {
|
|
765
|
+
aggregate: { id: string };
|
|
766
|
+
obligation: { id: string };
|
|
767
|
+
complete: { id: string };
|
|
768
|
+
fail: { id: string };
|
|
769
|
+
block: { id: string };
|
|
770
|
+
deadLetter: { id: string };
|
|
771
|
+
name?: unknown;
|
|
772
|
+
capability?: unknown;
|
|
773
|
+
endpoint?: unknown;
|
|
774
|
+
tasks?: { id?: unknown };
|
|
775
|
+
};
|
|
776
|
+
const cap = typeof k.name === "string" ? k.name : typeof k.capability === "string" ? k.capability : null;
|
|
777
|
+
if (cap === null) return null; // a binding-free quartet declares nothing (omit-when-absent)
|
|
778
|
+
return {
|
|
779
|
+
capability: cap,
|
|
780
|
+
aggregateId: k.aggregate.id,
|
|
781
|
+
obligation: k.obligation.id,
|
|
782
|
+
complete: k.complete.id,
|
|
783
|
+
fail: k.fail.id,
|
|
784
|
+
block: k.block.id,
|
|
785
|
+
deadLetter: k.deadLetter.id,
|
|
786
|
+
tasksQueryId: typeof k.tasks?.id === "string" ? k.tasks.id : null,
|
|
787
|
+
...(typeof k.endpoint === "string" ? { endpoint: k.endpoint } : {}),
|
|
788
|
+
};
|
|
789
|
+
})
|
|
790
|
+
.filter((c): c is NonNullable<typeof c> => c !== null)
|
|
791
|
+
.sort((a, b) => (a.aggregateId < b.aggregateId ? -1 : a.aggregateId > b.aggregateId ? 1 : 0));
|
|
792
|
+
// THE DECLASSIFY SITES (nomosFlowGate rule 4 — security_model.md): every acknowledged
|
|
793
|
+
// intended secret-correlated refusal, with its human reason. Hashed into the law (it IS
|
|
794
|
+
// a law-declared waiver) + replay-verified — "the set of declassify sites is the entire
|
|
795
|
+
// audit surface". Omit-when-empty: a domain with no declassify is byte-identical.
|
|
796
|
+
const flowDeclassify = [
|
|
797
|
+
...[...(l.module.workspaceInvariants ?? [])]
|
|
798
|
+
.filter((wi) => typeof (wi as { declassify?: unknown }).declassify === "string")
|
|
799
|
+
.map((wi) => ({ site: `workspaceInvariant:${wi.id}`, reason: (wi as { declassify: string }).declassify })),
|
|
800
|
+
...[...(l.module.aggregates as { id: string; invariantDeclassify?: unknown }[])]
|
|
801
|
+
.filter((a) => typeof a.invariantDeclassify === "string")
|
|
802
|
+
.map((a) => ({ site: `aggregateInvariant:${a.id}`, reason: a.invariantDeclassify as string })),
|
|
803
|
+
...[...((l.module as { impureCapabilities?: readonly { name?: string; capability?: string; aggregate: { id: string }; declassify?: unknown }[] }).impureCapabilities ?? [])]
|
|
804
|
+
.filter((c) => typeof c.declassify === "string")
|
|
805
|
+
.map((c) => ({ site: `capability:${c.name ?? c.capability ?? c.aggregate.id}`, reason: c.declassify as string })),
|
|
806
|
+
].sort((a, b) => (a.site < b.site ? -1 : a.site > b.site ? 1 : 0));
|
|
807
|
+
// THE ENDORSE FLOWS (the integrity dual — security_model.md): discovery IS law. Run each
|
|
808
|
+
// directive's pure plan with a probe principal + payload and harvest every PrincipalEvidence
|
|
809
|
+
// conversion's declared flow {directiveId, sink, facet, reason}. Hashed into nomosFlowGate +
|
|
810
|
+
// witnessed fail-closed at runtime (Slice B). Omit-when-empty (a principal-free domain is
|
|
811
|
+
// byte-identical). SORTED for determinism.
|
|
812
|
+
const flowEndorse = [...l.module.directives]
|
|
813
|
+
.flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
|
|
814
|
+
.sort((a, b) =>
|
|
815
|
+
a.directiveId !== b.directiveId ? (a.directiveId < b.directiveId ? -1 : 1)
|
|
816
|
+
: a.sink !== b.sink ? (a.sink < b.sink ? -1 : 1)
|
|
817
|
+
: a.facet < b.facet ? -1 : a.facet > b.facet ? 1 : 0,
|
|
818
|
+
);
|
|
703
819
|
return {
|
|
704
820
|
path: l.path,
|
|
705
821
|
prims: encodeModuleToPrims(l.path, l.module),
|
|
@@ -710,6 +826,9 @@ export function emitUsd(
|
|
|
710
826
|
...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
|
|
711
827
|
// STABLE IDS (#58): every post-#58 compile carries them (the era key).
|
|
712
828
|
...(manifest.stableIds !== undefined ? { nomosStableIds: manifest.stableIds } : {}),
|
|
829
|
+
// THE STRUCTURAL INTERFACE (#72): the on-ledger OFFER — a client negotiates shape-
|
|
830
|
+
// compatibility against this, reactively, straight from the folded `policy:{hash}.bundle`.
|
|
831
|
+
nomosInterface: domainInterface(l.module, manifest.stableIds),
|
|
713
832
|
...(Object.keys(readGateDirectives).length > 0
|
|
714
833
|
? {
|
|
715
834
|
nomosReadGate: {
|
|
@@ -737,6 +856,18 @@ export function emitUsd(
|
|
|
737
856
|
...(Object.keys(encAggregates).length > 0
|
|
738
857
|
? { nomosEncryption: { v: 1 as const, aggregates: encAggregates } }
|
|
739
858
|
: {}),
|
|
859
|
+
...(nomosCapabilities.length > 0
|
|
860
|
+
? { nomosCapabilities: { v: 1 as const, capabilities: nomosCapabilities } }
|
|
861
|
+
: {}),
|
|
862
|
+
...(flowDeclassify.length > 0 || flowEndorse.length > 0
|
|
863
|
+
? {
|
|
864
|
+
nomosFlowGate: {
|
|
865
|
+
v: 1 as const,
|
|
866
|
+
...(flowDeclassify.length > 0 ? { declassify: flowDeclassify } : {}),
|
|
867
|
+
...(flowEndorse.length > 0 ? { endorse: flowEndorse } : {}),
|
|
868
|
+
},
|
|
869
|
+
}
|
|
870
|
+
: {}),
|
|
740
871
|
};
|
|
741
872
|
})
|
|
742
873
|
.sort((a, b) => (a.path < b.path ? -1 : a.path > b.path ? 1 : 0));
|
package/src/usd_layers.ts
CHANGED
|
@@ -156,6 +156,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
|
|
|
156
156
|
...(layer.nomosStableIds !== undefined
|
|
157
157
|
? [`${indent}custom string nomos:stableIdsHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosStableIds)))}`]
|
|
158
158
|
: []),
|
|
159
|
+
...(layer.nomosInterface !== undefined
|
|
160
|
+
? [`${indent}custom string nomos:interfaceHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosInterface)))}`]
|
|
161
|
+
: []),
|
|
159
162
|
...(layer.nomosReadGate !== undefined
|
|
160
163
|
? [`${indent}custom string nomos:readGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosReadGate)))}`]
|
|
161
164
|
: []),
|
|
@@ -635,6 +638,37 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
635
638
|
stableIds = { v: 1, domain: domainSid, aggregates };
|
|
636
639
|
}
|
|
637
640
|
}
|
|
641
|
+
// THE STRUCTURAL INTERFACE (#72): fold per-module interfaces — directives + aggregates merged
|
|
642
|
+
// later-wins, SORTED keys (matching the composed emission's sorted-key `domainInterface`).
|
|
643
|
+
let iface: UsdLayer["nomosInterface"];
|
|
644
|
+
{
|
|
645
|
+
type Dir = NonNullable<UsdLayer["nomosInterface"]>["directives"][string];
|
|
646
|
+
type Agg = NonNullable<UsdLayer["nomosInterface"]>["aggregates"][string];
|
|
647
|
+
const dirs = new Map<string, Dir>();
|
|
648
|
+
const aggsI = new Map<string, Agg>();
|
|
649
|
+
let domainI: string | undefined;
|
|
650
|
+
let domainSidI: string | undefined;
|
|
651
|
+
for (const l of stack) {
|
|
652
|
+
if (l.nomosInterface === undefined) continue;
|
|
653
|
+
domainI = l.nomosInterface.domain;
|
|
654
|
+
domainSidI = l.nomosInterface.domainSid;
|
|
655
|
+
for (const [id, spec] of Object.entries(l.nomosInterface.directives)) dirs.set(id, spec);
|
|
656
|
+
for (const [sid, spec] of Object.entries(l.nomosInterface.aggregates)) aggsI.set(sid, spec);
|
|
657
|
+
}
|
|
658
|
+
if (domainI !== undefined) {
|
|
659
|
+
const directivesRec: Record<string, Dir> = {};
|
|
660
|
+
for (const k of [...dirs.keys()].sort()) directivesRec[k] = dirs.get(k)!;
|
|
661
|
+
const aggregatesRec: Record<string, Agg> = {};
|
|
662
|
+
for (const k of [...aggsI.keys()].sort()) aggregatesRec[k] = aggsI.get(k)!;
|
|
663
|
+
iface = {
|
|
664
|
+
v: 1,
|
|
665
|
+
domain: domainI,
|
|
666
|
+
...(domainSidI !== undefined ? { domainSid: domainSidI } : {}),
|
|
667
|
+
directives: directivesRec,
|
|
668
|
+
aggregates: aggregatesRec,
|
|
669
|
+
};
|
|
670
|
+
}
|
|
671
|
+
}
|
|
638
672
|
const identityManifest = foldIdentityManifest(stack, stableIds);
|
|
639
673
|
const readProjection = foldReadProjection(stack);
|
|
640
674
|
// The captured-read gate (#58): queries + per-directive read sets merged
|
|
@@ -740,6 +774,7 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
740
774
|
...(deriveds !== undefined ? { deriveds } : {}),
|
|
741
775
|
...(combineds !== undefined ? { combineds } : {}),
|
|
742
776
|
...(stableIds !== undefined ? { nomosStableIds: stableIds } : {}),
|
|
777
|
+
...(iface !== undefined ? { nomosInterface: iface } : {}),
|
|
743
778
|
...(readGate !== undefined ? { nomosReadGate: readGate } : {}),
|
|
744
779
|
...(readVisibility !== undefined ? { nomosReadVisibility: readVisibility } : {}),
|
|
745
780
|
...(encryption !== undefined ? { nomosEncryption: encryption } : {}),
|
|
@@ -866,6 +901,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
866
901
|
const identityManifestByDomain = new Map<string, UsdLayer["nomosIdentityManifest"]>();
|
|
867
902
|
const readProjectionByDomain = new Map<string, UsdLayer["nomosReadProjection"]>();
|
|
868
903
|
const stableIdsByDomain = new Map<string, UsdLayer["nomosStableIds"]>();
|
|
904
|
+
const interfaceByDomain = new Map<string, UsdLayer["nomosInterface"]>();
|
|
869
905
|
const readGateByDomain = new Map<string, UsdLayer["nomosReadGate"]>();
|
|
870
906
|
const readVisibilityByDomain = new Map<string, UsdLayer["nomosReadVisibility"]>();
|
|
871
907
|
const encryptionByDomain = new Map<string, UsdLayer["nomosEncryption"]>();
|
|
@@ -889,6 +925,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
889
925
|
if (typeof sidHex === "string") {
|
|
890
926
|
stableIdsByDomain.set(domain, JSON.parse(unhexUtf8(sidHex)) as UsdLayer["nomosStableIds"]);
|
|
891
927
|
}
|
|
928
|
+
// THE STRUCTURAL INTERFACE (#72): the on-ledger OFFER, hex-carried per domain layer.
|
|
929
|
+
const ifaceHex = p.attrs.get("nomos:interfaceHex");
|
|
930
|
+
if (typeof ifaceHex === "string") {
|
|
931
|
+
interfaceByDomain.set(domain, JSON.parse(unhexUtf8(ifaceHex)) as UsdLayer["nomosInterface"]);
|
|
932
|
+
}
|
|
892
933
|
const rgHex = p.attrs.get("nomos:readGateHex");
|
|
893
934
|
if (typeof rgHex === "string") {
|
|
894
935
|
readGateByDomain.set(domain, JSON.parse(unhexUtf8(rgHex)) as UsdLayer["nomosReadGate"]);
|
|
@@ -1008,6 +1049,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1008
1049
|
const identity = identityManifestByDomain.get(domain);
|
|
1009
1050
|
const readProjection = readProjectionByDomain.get(domain);
|
|
1010
1051
|
const sids = stableIdsByDomain.get(domain);
|
|
1052
|
+
const iface = interfaceByDomain.get(domain);
|
|
1011
1053
|
const rg = readGateByDomain.get(domain);
|
|
1012
1054
|
const rv = readVisibilityByDomain.get(domain);
|
|
1013
1055
|
const enc = encryptionByDomain.get(domain);
|
|
@@ -1023,6 +1065,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1023
1065
|
...(b.deriveds.length > 0 ? { deriveds: [...b.deriveds].sort(byId) } : {}),
|
|
1024
1066
|
...(b.combineds.length > 0 ? { combineds: [...b.combineds].sort(byId) } : {}),
|
|
1025
1067
|
...(sids !== undefined ? { nomosStableIds: sids } : {}),
|
|
1068
|
+
...(iface !== undefined ? { nomosInterface: iface } : {}),
|
|
1026
1069
|
...(rg !== undefined ? { nomosReadGate: rg } : {}),
|
|
1027
1070
|
...(rv !== undefined ? { nomosReadVisibility: rv } : {}),
|
|
1028
1071
|
...(enc !== undefined ? { nomosEncryption: enc } : {}),
|
package/src/wire.ts
CHANGED
|
@@ -127,6 +127,14 @@ export interface WireIntent {
|
|
|
127
127
|
strikes: string[];
|
|
128
128
|
events: WireEvent[];
|
|
129
129
|
reads?: WireRead[];
|
|
130
|
+
/**
|
|
131
|
+
* THE OBSERVED ENDORSE FOOTPRINT (security_model.md — the integrity dual, Slice B): every
|
|
132
|
+
* PrincipalEvidence flow this plan run actually produced `{sink, facet, reason}`. A TRANSIENT
|
|
133
|
+
* verify-input — it rides the plan output the gate checks against the law's `nomosFlowGate.endorse`
|
|
134
|
+
* (author + replay), NEVER the committed intent's events, so it is not OID-hashed. OMITTED when
|
|
135
|
+
* empty so a principal-free intent is byte-identical.
|
|
136
|
+
*/
|
|
137
|
+
endorse?: { sink: string; facet: string; reason: string | null }[];
|
|
130
138
|
}
|
|
131
139
|
|
|
132
140
|
/**
|
package/src/wire_encode.ts
CHANGED
|
@@ -26,7 +26,8 @@ import type { AggregateHandle } from "./aggregate.js";
|
|
|
26
26
|
import type { Field, EvidenceRefValue } from "./fields.js";
|
|
27
27
|
import type { Directive, ReferentialMarker } from "./directive.js";
|
|
28
28
|
import type { PlannedOp, FieldOp } from "./ops.js";
|
|
29
|
-
import type
|
|
29
|
+
import { deterministicPorts, type Ports } from "./ctx.js";
|
|
30
|
+
import type { PrincipalFacet } from "./principal_evidence.js";
|
|
30
31
|
import type {
|
|
31
32
|
WireEvent,
|
|
32
33
|
WireHlc,
|
|
@@ -290,5 +291,77 @@ export function executeDirectiveToIntent<P>(
|
|
|
290
291
|
return { aggregate, marker: eventMarker, ops };
|
|
291
292
|
});
|
|
292
293
|
// Attach the captured read footprint — OMITTED when empty so a read-free intent is byte-identical.
|
|
293
|
-
|
|
294
|
+
// Attach the OBSERVED ENDORSE footprint (Slice B) — the principal flows this run produced; the gate
|
|
295
|
+
// checks them against the law's nomosFlowGate.endorse. Transient verify-input (not OID-hashed),
|
|
296
|
+
// OMITTED when empty so a principal-free intent is byte-identical.
|
|
297
|
+
const endorse = flowsFromOps(ops);
|
|
298
|
+
return {
|
|
299
|
+
id: intentIdFromHlc(hlc),
|
|
300
|
+
hlc,
|
|
301
|
+
strikes,
|
|
302
|
+
events,
|
|
303
|
+
...(reads.length ? { reads } : {}),
|
|
304
|
+
...(endorse.length ? { endorse } : {}),
|
|
305
|
+
};
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
/** One declared principal flow harvested from a plan — `sink` is `<AggregateType>.<field>`. */
|
|
309
|
+
export interface PrincipalFlow {
|
|
310
|
+
readonly sink: string;
|
|
311
|
+
readonly facet: PrincipalFacet;
|
|
312
|
+
readonly reason: string | null;
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
/** Extract the principal flows carried by a plan's field ops (those with an `endorsement`). The ONE
|
|
316
|
+
* op→flow mapping shared by the discovery harvest (compile) and the runtime footprint (Slice B). */
|
|
317
|
+
function flowsFromOps(ops: readonly FieldOp[]): PrincipalFlow[] {
|
|
318
|
+
const flows: PrincipalFlow[] = [];
|
|
319
|
+
for (const op of ops) {
|
|
320
|
+
if (op.endorsement) {
|
|
321
|
+
flows.push({ sink: `${op.aggregateType ?? op.aggregateId}.${op.field}`, facet: op.endorsement.facet, reason: op.endorsement.reason });
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
return flows.sort((a, b) =>
|
|
325
|
+
a.sink !== b.sink ? (a.sink < b.sink ? -1 : 1) : a.facet < b.facet ? -1 : a.facet > b.facet ? 1 : 0,
|
|
326
|
+
);
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
/**
|
|
330
|
+
* THE DISCOVERY HARVEST (security_model.md — discovery IS law): run a directive's PURE plan with a
|
|
331
|
+
* probe payload + a probe principal, and harvest every PrincipalEvidence conversion's declared flow
|
|
332
|
+
* (`{sink, facet, reason}`). Executes the plan (not its AST) — the same technique as the rule-4 reads
|
|
333
|
+
* probe. Best-effort + fail-open on HARVEST (a probe that can't drive a branch just omits that flow);
|
|
334
|
+
* the omission is SAFE because the runtime/replay witness (Slice B) refuses any observed flow absent
|
|
335
|
+
* from the manifest. The reasons are static law, so they are intent-independent. SORTED for determinism.
|
|
336
|
+
*/
|
|
337
|
+
export function harvestPrincipalFlows(directive: Directive<unknown>): PrincipalFlow[] {
|
|
338
|
+
const ctx: Ports = deterministicPorts({ principal: "principal:probe" });
|
|
339
|
+
// A probe payload: any field access yields a benign string. Plans that need a typed shape (an
|
|
340
|
+
// array .map, etc.) may throw — caught below; that directive's flows are then witnessed at runtime.
|
|
341
|
+
const probe = new Proxy({}, { get: () => "probe" });
|
|
342
|
+
// A plan that MINTS (`create(Agg)` — the common `.creates` shape) calls `globalThis.nomos.mint`; the
|
|
343
|
+
// harvest runs at compile, where the engine's real shim is absent. Install a deterministic stub so the
|
|
344
|
+
// probe plan runs to completion and its flows ARE harvested (an un-harvested authority flow would be
|
|
345
|
+
// refused at runtime by the Slice B witness — fail-closed, but a false jam). The minted VALUE is
|
|
346
|
+
// irrelevant — a flow's sink is the field path, not the id. Restore any pre-existing shim after.
|
|
347
|
+
const g = globalThis as unknown as { nomos?: { mint?: (t: string) => string; read?: unknown } };
|
|
348
|
+
const hadNomos = "nomos" in g;
|
|
349
|
+
const prior: { mint?: (t: string) => string; read?: unknown } | undefined = g.nomos;
|
|
350
|
+
if (!hadNomos || typeof prior?.mint !== "function") {
|
|
351
|
+
g.nomos = { ...(prior ?? {}), mint: (typeTag: string) => `${typeTag}_0192f00d-0000-7000-8000-000000000000` };
|
|
352
|
+
}
|
|
353
|
+
let ops: FieldOp[];
|
|
354
|
+
__resetAuthoring();
|
|
355
|
+
__resetReads();
|
|
356
|
+
try {
|
|
357
|
+
const returned = directive.plan(probe as never, ctx);
|
|
358
|
+
ops = [...__drainAuthoring(), ...(Array.isArray(returned) ? returned : [])].filter((op): op is FieldOp => op.kind !== "strike");
|
|
359
|
+
} catch {
|
|
360
|
+
try { __drainAuthoring(); __drainReads(); } catch { /* sink already drained */ }
|
|
361
|
+
return [];
|
|
362
|
+
} finally {
|
|
363
|
+
if (hadNomos && prior !== undefined) g.nomos = prior; else delete g.nomos;
|
|
364
|
+
}
|
|
365
|
+
__drainReads();
|
|
366
|
+
return flowsFromOps(ops);
|
|
294
367
|
}
|