@githolon/dsl 0.80.1 → 0.81.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -1
- package/src/births.ts +75 -0
- package/src/compile_package_main.ts +22 -0
- package/src/dart_barrel_guard.ts +78 -0
- package/src/directive.ts +75 -0
- package/src/secondary_ensure.ts +13 -1
- package/src/usd.ts +44 -0
- package/src/usd_layers.ts +28 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@githolon/dsl",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.81.0",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
|
|
6
6
|
"license": "SEE LICENSE IN LICENSE.md",
|
package/src/births.ts
CHANGED
|
@@ -79,6 +79,81 @@ export function birth(recipe: BirthRecipe): void {
|
|
|
79
79
|
footprint.push(recipe);
|
|
80
80
|
}
|
|
81
81
|
|
|
82
|
+
// `nomos.sha256` — the trustless pure hash bound in the sealed engine (like `nomos.mint`), used to derive a
|
|
83
|
+
// singleton child's DETERMINISTIC name in-law so every peer re-derives the same name.
|
|
84
|
+
declare const nomos: { sha256(input: string): string };
|
|
85
|
+
|
|
86
|
+
/** A genesis chain, or a builder that receives the resolved child workspace name (so a seed step can embed
|
|
87
|
+
* it — e.g. `seedEstate` needs to write its own `estateName`). Passing a builder is the common case. */
|
|
88
|
+
export type GenesisChainOrBuilder = unknown | ((childWorkspace: string) => unknown);
|
|
89
|
+
function resolveChain(child: string, c: GenesisChainOrBuilder): unknown {
|
|
90
|
+
return typeof c === "function" ? (c as (w: string) => unknown)(child) : c;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/** A `create()`d aggregate reference — the birth's audit record, whose MINTED (captured, replay-stable) id
|
|
94
|
+
* becomes the instance child's workspace name. Only its `__id` is read. */
|
|
95
|
+
interface MintedRef {
|
|
96
|
+
readonly __id: string;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* THE BIRTH CARDINALITY DECISION — stated at the call site so an agent models it right by default.
|
|
101
|
+
*
|
|
102
|
+
* A born child's workspace name is its IDENTITY. The wrong instinct (births are deterministic, so derive the
|
|
103
|
+
* name from a stable field like the owner) silently caps the parent at ONE child per owner. So the choice is
|
|
104
|
+
* explicit and NAMED, exactly like a merge driver states convergence:
|
|
105
|
+
*
|
|
106
|
+
* • `birth.instance(...)` — MANY children per parent. The child's workspace id is MINTED from `from` (an
|
|
107
|
+
* aggregate you `create()` for the birth) — its id already rides the committed intent (captured), so the
|
|
108
|
+
* child name reproduces byte-identically at replay. Nomos mints the identity; you never hand-roll it.
|
|
109
|
+
*
|
|
110
|
+
* • `birth.singleton(...)` — EXACTLY ONE child per `keyedBy`. The name is DERIVED deterministically in-law,
|
|
111
|
+
* so a re-birth is idempotent (same name → the child self-validates to the same identity). This is the
|
|
112
|
+
* RIGHT tool for a home (one home per user), and the ONLY case where a name-from-a-field is correct.
|
|
113
|
+
*
|
|
114
|
+
* Raw {@link birth} stays the low-level escape hatch for an externally-determined name.
|
|
115
|
+
*/
|
|
116
|
+
export const birthInstance = ({
|
|
117
|
+
from,
|
|
118
|
+
prefix = "",
|
|
119
|
+
genesisChain,
|
|
120
|
+
}: {
|
|
121
|
+
/** The `create()`d audit record whose minted, captured id names the child (replay-stable uniqueness). */
|
|
122
|
+
from: MintedRef;
|
|
123
|
+
/** Optional human-legible workspace-name prefix (e.g. `"estate-"`). */
|
|
124
|
+
prefix?: string;
|
|
125
|
+
genesisChain: GenesisChainOrBuilder;
|
|
126
|
+
}): string => {
|
|
127
|
+
const child = prefix + from.__id;
|
|
128
|
+
footprint.push({ workspace: child, genesisChain: resolveChain(child, genesisChain) });
|
|
129
|
+
return child;
|
|
130
|
+
};
|
|
131
|
+
|
|
132
|
+
export const birthSingleton = ({
|
|
133
|
+
keyedBy,
|
|
134
|
+
namespace,
|
|
135
|
+
genesisChain,
|
|
136
|
+
}: {
|
|
137
|
+
/** The key the child is a singleton OF (e.g. the owner). One child per distinct key; re-birth idempotent. */
|
|
138
|
+
keyedBy: string | readonly string[];
|
|
139
|
+
/** Optional name namespace/prefix (e.g. `"home"` → `home-<hash>`). */
|
|
140
|
+
namespace?: string;
|
|
141
|
+
genesisChain: GenesisChainOrBuilder;
|
|
142
|
+
}): string => {
|
|
143
|
+
const parts = (Array.isArray(keyedBy) ? keyedBy : [keyedBy]).map(String);
|
|
144
|
+
const child = (namespace ? namespace + "-" : "") + nomos.sha256(JSON.stringify(parts)).slice(0, 40);
|
|
145
|
+
footprint.push({ workspace: child, genesisChain: resolveChain(child, genesisChain) });
|
|
146
|
+
return child;
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Function/namespace declaration merging: attach the two cardinality verbs to `birth` so the call site reads
|
|
150
|
+
// `birth.instance(...)` / `birth.singleton(...)` — with full types AND the runtime assignment in one place.
|
|
151
|
+
// eslint-disable-next-line @typescript-eslint/no-namespace
|
|
152
|
+
export namespace birth {
|
|
153
|
+
export const instance = birthInstance;
|
|
154
|
+
export const singleton = birthSingleton;
|
|
155
|
+
}
|
|
156
|
+
|
|
82
157
|
/**
|
|
83
158
|
* THE PEER-SOVEREIGN ROOT-OF-TRUST SENTINEL (the BirthAttestation cert chain). A home/child law EXPORTS
|
|
84
159
|
* this marker to declare it REQUIRES a parent-attested-birth: a warranted workspace running this law must
|
|
@@ -124,6 +124,7 @@ import {
|
|
|
124
124
|
} from "./stable_ids.js";
|
|
125
125
|
import { emitUsd, type UsdLayer } from "./usd.js";
|
|
126
126
|
import { emitLayeredRelationUsda, type RelationLayerInput } from "./relation_composition.js";
|
|
127
|
+
import { assertNoDartBarrelCollisions, type BarrelExportedFile } from "./dart_barrel_guard.js";
|
|
127
128
|
import { generateTsClient, tsClientFactoryName, recursiveLazyTypes } from "./codegen_ts.js";
|
|
128
129
|
import { generateOpenApiFragment, mergeOpenApiFragments } from "./codegen_openapi.js";
|
|
129
130
|
import { domainInterface, type DomainInterface } from "./shape_compat.js";
|
|
@@ -892,6 +893,17 @@ function emitDart(
|
|
|
892
893
|
// dedup above makes the exports collision-free by construction; re-exporting the same library
|
|
893
894
|
// through two paths — e.g. a client's own `nomos_framework.dart` re-export — is not ambiguous).
|
|
894
895
|
const genRel = DART_GENERATED_SUBDIR.split(path.sep).join("/");
|
|
896
|
+
// STRUCTURAL GUARD (dart_barrel_guard.ts): the barrel flat-exports every generated TYPES file, so no
|
|
897
|
+
// public name may be declared in ≥2 of them — else `dart analyze` fails the package with
|
|
898
|
+
// ambiguous_export. Refuse the compile NOW (before we write an uncompilable surface) with a named,
|
|
899
|
+
// actionable error. The framework-shared dedup keeps this green; this catches any future gap in it.
|
|
900
|
+
try {
|
|
901
|
+
assertNoDartBarrelCollisions(
|
|
902
|
+
[...sourceByFile.entries()].map(([label, src]): BarrelExportedFile => ({ label, src })),
|
|
903
|
+
);
|
|
904
|
+
} catch (e) {
|
|
905
|
+
fail((e as Error).message);
|
|
906
|
+
}
|
|
895
907
|
const barrelLines = [
|
|
896
908
|
"// GENERATED by nomos-compile — the package's public entrypoint. DO NOT EDIT.",
|
|
897
909
|
"// Regenerated on every compile; generated implementation lives under src/generated/.",
|
|
@@ -1196,6 +1208,16 @@ function emitDartPackages(
|
|
|
1196
1208
|
`// Import a domain with a prefix instead, e.g. \`import '${genRel}/<domain>.dart' as <domain>;\`.`,
|
|
1197
1209
|
]
|
|
1198
1210
|
: [];
|
|
1211
|
+
// STRUCTURAL GUARD (dart_barrel_guard.ts): refuse a types barrel that would flat-export the SAME public
|
|
1212
|
+
// name from ≥2 files (ambiguous_export). Only the files the barrel ACTUALLY flat-exports are checked —
|
|
1213
|
+
// under `barrelExports: 'explicit'` per-domain files are withheld, so they cannot collide there.
|
|
1214
|
+
try {
|
|
1215
|
+
assertNoDartBarrelCollisions(
|
|
1216
|
+
typesExportKeys.map((f): BarrelExportedFile => ({ label: f, src: typesSources.get(f) ?? "" })),
|
|
1217
|
+
);
|
|
1218
|
+
} catch (e) {
|
|
1219
|
+
fail((e as Error).message);
|
|
1220
|
+
}
|
|
1199
1221
|
const typesBarrel = [
|
|
1200
1222
|
"// GENERATED by nomos-compile — the TYPES package's public entrypoint. DO NOT EDIT.",
|
|
1201
1223
|
"// Pure data: payload classes, read models, VOs/enums, typed read accessors over the",
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon wasm.
|
|
3
|
+
// If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* DART BARREL COLLISION GUARD — compile self-refuses an uncompilable Dart surface.
|
|
7
|
+
*
|
|
8
|
+
* A Nomos package's generated Dart is exposed through ONE package barrel that FLAT-EXPORTS each
|
|
9
|
+
* generated file (`export 'src/generated/estate.dart';`, …). Dart's `export` brings every PUBLIC
|
|
10
|
+
* top-level declaration into the barrel's namespace; if the SAME public name is declared in ≥2
|
|
11
|
+
* flat-exported files, `dart analyze` fails the whole package with `ambiguous_export` — an error the
|
|
12
|
+
* tenant cannot fix without hand-editing generated code or restructuring their package.
|
|
13
|
+
*
|
|
14
|
+
* The framework-shared dedup (`nomos_framework.dart`) is meant to make this impossible by
|
|
15
|
+
* construction (framework symbols emit ONCE), but any gap in that dedup — a framework payload class
|
|
16
|
+
* NOT routed to the shared file, a VO emitted per-domain — reintroduces the collision silently. This
|
|
17
|
+
* guard is the STRUCTURAL backstop: we KNOW the public names we emit and which files the barrel
|
|
18
|
+
* flat-exports, so we detect the ambiguity at COMPILE time (no `dart analyze` needed) and REFUSE with
|
|
19
|
+
* a named, actionable error. Third occurrence of this bug class in layered Dart — this closes it for good.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
/** One flat-exported generated file the barrel re-exports: a human label (the domain / file name) + its Dart source. */
|
|
23
|
+
export interface BarrelExportedFile {
|
|
24
|
+
/** The file's identifying label used in the error message (e.g. the domain key `estate`). */
|
|
25
|
+
readonly label: string;
|
|
26
|
+
/** The generated Dart source. */
|
|
27
|
+
readonly src: string;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Declared PUBLIC top-level type/typedef names in a Dart source (class/enum/mixin/extension type/typedef).
|
|
31
|
+
* Private (`_`-prefixed) names never leak through an `export`, so they cannot collide — excluded. */
|
|
32
|
+
export function publicDartDeclarations(src: string): Set<string> {
|
|
33
|
+
const names = new Set<string>();
|
|
34
|
+
const re =
|
|
35
|
+
/^(?:abstract\s+|base\s+|final\s+|sealed\s+|interface\s+|mixin\s+)*(?:class|enum|mixin|extension type|typedef)\s+([A-Za-z][A-Za-z0-9_]*)/gm;
|
|
36
|
+
for (let m = re.exec(src); m !== null; m = re.exec(src)) {
|
|
37
|
+
if (!m[1]!.startsWith("_")) names.add(m[1]!);
|
|
38
|
+
}
|
|
39
|
+
return names;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** name → the sorted labels of the flat-exported files that each DECLARE it. Only entries with ≥2 files
|
|
43
|
+
* are collisions (an `ambiguous_export`); a name declared in exactly one file is fine. */
|
|
44
|
+
export function findDartBarrelCollisions(
|
|
45
|
+
files: readonly BarrelExportedFile[],
|
|
46
|
+
): Map<string, string[]> {
|
|
47
|
+
const byName = new Map<string, Set<string>>();
|
|
48
|
+
for (const f of files) {
|
|
49
|
+
for (const name of publicDartDeclarations(f.src)) {
|
|
50
|
+
const set = byName.get(name) ?? new Set<string>();
|
|
51
|
+
set.add(f.label);
|
|
52
|
+
byName.set(name, set);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
const collisions = new Map<string, string[]>();
|
|
56
|
+
for (const [name, labels] of byName) {
|
|
57
|
+
if (labels.size >= 2) collisions.set(name, [...labels].sort());
|
|
58
|
+
}
|
|
59
|
+
return collisions;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** THROW a named, actionable error if the flat-exported files declare any public name more than once.
|
|
63
|
+
* Green (no-op) when the surface is collision-free — the state every valid package MUST be in. */
|
|
64
|
+
export function assertNoDartBarrelCollisions(files: readonly BarrelExportedFile[]): void {
|
|
65
|
+
const collisions = findDartBarrelCollisions(files);
|
|
66
|
+
if (collisions.size === 0) return;
|
|
67
|
+
const lines = [...collisions.entries()]
|
|
68
|
+
.sort(([a], [b]) => (a < b ? -1 : 1))
|
|
69
|
+
.map(([name, labels]) => ` • '${name}' is declared in ${labels.map((l) => `'${l}'`).join(", ")}`);
|
|
70
|
+
throw new Error(
|
|
71
|
+
`generated Dart would not compile — the package barrel flat-exports the SAME public name from ` +
|
|
72
|
+
`multiple files, so 'dart analyze' fails with ambiguous_export:\n${lines.join("\n")}\n` +
|
|
73
|
+
`A public name emitted per-domain must be FRAMEWORK-SHARED (emitted ONCE in nomos_framework.dart) ` +
|
|
74
|
+
`— this is a compiler bug, not a tenant one; do not hand-edit generated code. If a FRAMEWORK ` +
|
|
75
|
+
`directive/aggregate/value-object payload is colliding, it must carry the framework brand so the ` +
|
|
76
|
+
`dedup routes it to the shared file (see framework_aggregates.ts / secondary_ensure.ts).`,
|
|
77
|
+
);
|
|
78
|
+
}
|
package/src/directive.ts
CHANGED
|
@@ -169,6 +169,28 @@ export interface Directive<P = unknown> {
|
|
|
169
169
|
readonly signature: string;
|
|
170
170
|
};
|
|
171
171
|
};
|
|
172
|
+
/**
|
|
173
|
+
* DELEGATED-AUTHORITY DELIVERY (`delegatedRole`/`.delegatedFrom(parent)`,
|
|
174
|
+
* architecture/delegated_authority_births.md). When set, this directive folds a DELEGATED relation tuple
|
|
175
|
+
* ONLY if its payload carries a valid `DelegationCert` — a platform P's SIGNED grant, rooting at `K_root`,
|
|
176
|
+
* binding the SAME (relation, subject) the tuple writes, and (`.delegatedFrom(parent)`) whose grantor key
|
|
177
|
+
* is THIS workspace's declared parent key. Lowered into the `nomosDelegationGate` era key so the one gate
|
|
178
|
+
* (author / admission / verify_chain) re-verifies the grant OFFLINE — the delivered fact H's own
|
|
179
|
+
* `.requires(relation)` birth gate then reads locally. The kernel hardcodes no id/field — all law-declared.
|
|
180
|
+
*/
|
|
181
|
+
readonly delegatedRoleGate?: {
|
|
182
|
+
/** The delegated relation the folded tuple writes (`co2EstateCreator`) — the cert must grant exactly it. */
|
|
183
|
+
readonly relation: string;
|
|
184
|
+
/** The payload field carrying the delegated subject (a `user:`/`key:` string). */
|
|
185
|
+
readonly subjectField: string;
|
|
186
|
+
/** The payload field carrying the `DelegationCert` (JSON string). */
|
|
187
|
+
readonly certField: string;
|
|
188
|
+
/** `.delegatedFrom(parent)`: the singleton INSTANCE id whose field holds this workspace's parent key
|
|
189
|
+
* (e.g. `birth-attestation:self` / a seeded `platform-key:self`). "" ⇒ from-parent binding inert. */
|
|
190
|
+
readonly parentKeyObject: string;
|
|
191
|
+
/** The field on that singleton holding the parent's base64 ed25519 public key (e.g. `parentKey`). */
|
|
192
|
+
readonly parentKeyField: string;
|
|
193
|
+
};
|
|
172
194
|
/**
|
|
173
195
|
* THE KEYLESS VERIFIED-FIRST-DEVICE GATE (architecture/keyless_verified_onboard.md). When set, this
|
|
174
196
|
* directive bootstraps a principal's FIRST device-signer from an ON-CHAIN, device-bound IdP attestation:
|
|
@@ -365,6 +387,21 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
365
387
|
* host-free. Framework-only (the home law's `recordShare`). Returns a NEW requirable directive.
|
|
366
388
|
*/
|
|
367
389
|
attestationAuthorized(owner: { ownerObject: string; ownerField: string }): RequirableDirective<P>;
|
|
390
|
+
/**
|
|
391
|
+
* Declare this directive a DELEGATED-AUTHORITY delivery (`delegated_authority_births.md`): its write folds
|
|
392
|
+
* the delegated `relation` tuple ONLY behind a carried `DelegationCert` P signed — lowered into the
|
|
393
|
+
* `nomosDelegationGate` era key so the one gate re-verifies the grant to `K_root` OFFLINE on every peer.
|
|
394
|
+
* `subjectField`/`certField` default to `"subject"`/`"delegationCert"`. Chain `.delegatedFrom(parent)` to
|
|
395
|
+
* bind the grantor to this workspace's parent key. Returns a NEW requirable directive.
|
|
396
|
+
*/
|
|
397
|
+
delegatedRole(relation: string, opts?: { subjectField?: string; certField?: string }): RequirableDirective<P>;
|
|
398
|
+
/**
|
|
399
|
+
* `.delegatedFrom(parent)` — bind the delegating grantor to THIS workspace's parent key (the platform P
|
|
400
|
+
* that bore this home): the `DelegationCert`'s grantor key MUST equal the base64 key at
|
|
401
|
+
* `parentKeyObject.parentKeyField` (e.g. the birth-cert `birth-attestation:self`.`parentKey`, or a seeded
|
|
402
|
+
* `platform-key:self`.`grantorKey`). Refines the `.delegatedRole()` gate. Returns a NEW requirable directive.
|
|
403
|
+
*/
|
|
404
|
+
delegatedFrom(parent: { parentKeyObject: string; parentKeyField: string }): RequirableDirective<P>;
|
|
368
405
|
/**
|
|
369
406
|
* Declare this directive a KEYLESS VERIFIED-FIRST-DEVICE bootstrap (keyless_verified_onboard.md): its
|
|
370
407
|
* authorization is a carried, device-bound IdP `idpToken` re-verified against the workspace AuthProvider —
|
|
@@ -762,6 +799,44 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
|
762
799
|
},
|
|
763
800
|
});
|
|
764
801
|
},
|
|
802
|
+
delegatedRole(relation: string, opts?: { subjectField?: string; certField?: string }): RequirableDirective<P> {
|
|
803
|
+
// DELEGATED-AUTHORITY DELIVERY (delegated_authority_births.md): this directive's write folds the
|
|
804
|
+
// delegated `relation` tuple ONLY behind a carried DelegationCert — lowered into the `nomosDelegationGate`
|
|
805
|
+
// era key, so the one gate re-verifies P's grant to K_root OFFLINE on every peer. Field names default to
|
|
806
|
+
// the framework convention here (the kernel reads them from the law's declaration, hardcoding nothing).
|
|
807
|
+
const prior = (base as { delegatedRoleGate?: { parentKeyObject?: string; parentKeyField?: string } })
|
|
808
|
+
.delegatedRoleGate;
|
|
809
|
+
return makeRequirable({
|
|
810
|
+
...base,
|
|
811
|
+
delegatedRoleGate: {
|
|
812
|
+
relation,
|
|
813
|
+
subjectField: opts?.subjectField ?? "subject",
|
|
814
|
+
certField: opts?.certField ?? "delegationCert",
|
|
815
|
+
parentKeyObject: prior?.parentKeyObject ?? "",
|
|
816
|
+
parentKeyField: prior?.parentKeyField ?? "",
|
|
817
|
+
},
|
|
818
|
+
});
|
|
819
|
+
},
|
|
820
|
+
delegatedFrom(parent: { parentKeyObject: string; parentKeyField: string }): RequirableDirective<P> {
|
|
821
|
+
// .delegatedFrom(parent): bind the grantor to this workspace's parent key (the platform P). Refines the
|
|
822
|
+
// .delegatedRole() gate (a preceding .delegatedRole() supplies relation/fields; this adds the binding).
|
|
823
|
+
const g = (base as { delegatedRoleGate?: {
|
|
824
|
+
relation: string; subjectField: string; certField: string;
|
|
825
|
+
} }).delegatedRoleGate;
|
|
826
|
+
if (g === undefined) {
|
|
827
|
+
throw new Error("directive.delegatedFrom(): call .delegatedRole(relation) before .delegatedFrom(parent)");
|
|
828
|
+
}
|
|
829
|
+
return makeRequirable({
|
|
830
|
+
...base,
|
|
831
|
+
delegatedRoleGate: {
|
|
832
|
+
relation: g.relation,
|
|
833
|
+
subjectField: g.subjectField,
|
|
834
|
+
certField: g.certField,
|
|
835
|
+
parentKeyObject: parent.parentKeyObject,
|
|
836
|
+
parentKeyField: parent.parentKeyField,
|
|
837
|
+
},
|
|
838
|
+
});
|
|
839
|
+
},
|
|
765
840
|
verifiedFirstDevice(opts?: {
|
|
766
841
|
tokenField?: string;
|
|
767
842
|
keyHashField?: string;
|
package/src/secondary_ensure.ts
CHANGED
|
@@ -53,10 +53,22 @@ export function sanctioningEnsureDirectiveId(aggId: string): string {
|
|
|
53
53
|
* lane for a plan's secondary deterministic `ensure(agg, …)`.
|
|
54
54
|
*/
|
|
55
55
|
export function sanctioningEnsureDirective(agg: AggregateHandle): Directive<unknown> {
|
|
56
|
-
|
|
56
|
+
const dir = directive(sanctioningEnsureDirectiveId(agg.id))
|
|
57
57
|
.ensures(agg)
|
|
58
58
|
.payload(z.object({}).strict())
|
|
59
59
|
.plan(() => []) as unknown as Directive<unknown>;
|
|
60
|
+
// FRAMEWORK-SHARED DEDUP (Dart codegen): when the sanctioned aggregate is a FRAMEWORK singleton
|
|
61
|
+
// (e.g. RelationTuple — auto-ensured into EVERY relation-using domain), this directive is injected
|
|
62
|
+
// per-domain but its payload class (`NomosEnsure_<Agg>Payload`, zero-field) is byte-identical across
|
|
63
|
+
// all of them. Left unbranded, each domain's Dart file re-defines the class and a layered types
|
|
64
|
+
// barrel flat-exporting ≥2 domains collides (`ambiguous_export`). Brand it framework — exactly the
|
|
65
|
+
// `nomosSourceFrontierEnsure` precedent — so its payload dedups into `nomos_framework.dart` ONCE.
|
|
66
|
+
// A sanction over a TENANT aggregate stays per-domain (its aggregate lives in that one domain, so no
|
|
67
|
+
// cross-domain collision is possible) — hence gating on the framework brand, not blanket branding.
|
|
68
|
+
if ((agg as { framework?: true }).framework === true) {
|
|
69
|
+
(dir as { framework?: true }).framework = true;
|
|
70
|
+
}
|
|
71
|
+
return dir;
|
|
60
72
|
}
|
|
61
73
|
|
|
62
74
|
/**
|
package/src/usd.ts
CHANGED
|
@@ -347,6 +347,25 @@ export interface UsdLayer {
|
|
|
347
347
|
}
|
|
348
348
|
>;
|
|
349
349
|
};
|
|
350
|
+
/**
|
|
351
|
+
* THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md). Present IFF ≥1 directive declares
|
|
352
|
+
* `.delegatedRole()`: directiveId → {relation, subjectField, certField, parentKeyObject, parentKeyField}.
|
|
353
|
+
* The one gate folds the delegated tuple ONLY behind a DelegationCert P signed (rooting at K_root, binding
|
|
354
|
+
* the same relation+subject, `.delegatedFrom(parent)`). OMITTED for law with no such directive (era rule).
|
|
355
|
+
*/
|
|
356
|
+
readonly nomosDelegationGate?: {
|
|
357
|
+
readonly v: 1;
|
|
358
|
+
readonly directives: Record<
|
|
359
|
+
string,
|
|
360
|
+
{
|
|
361
|
+
readonly relation: string;
|
|
362
|
+
readonly subjectField: string;
|
|
363
|
+
readonly certField: string;
|
|
364
|
+
readonly parentKeyObject: string;
|
|
365
|
+
readonly parentKeyField: string;
|
|
366
|
+
}
|
|
367
|
+
>;
|
|
368
|
+
};
|
|
350
369
|
/**
|
|
351
370
|
* THE KEYLESS VERIFIED-FIRST-DEVICE GATE (architecture/keyless_verified_onboard.md). Present IFF ≥1
|
|
352
371
|
* directive declares `.verifiedFirstDevice()`: a per-directive payload FIELD MAPPING. A principal's FIRST
|
|
@@ -1136,6 +1155,30 @@ export function emitUsd(
|
|
|
1136
1155
|
}
|
|
1137
1156
|
const nomosAttestationGate =
|
|
1138
1157
|
Object.keys(attestationReq).length > 0 ? { v: 1 as const, directives: attestationReq } : undefined;
|
|
1158
|
+
// THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md): directives declaring `.delegatedRole()`
|
|
1159
|
+
// → directiveId → its {relation, subjectField, certField, parentKeyObject, parentKeyField}. SORTED keys
|
|
1160
|
+
// (byte-determinism). Omit-when-empty (era rule) — a law with no delegation is byte-identical. The one
|
|
1161
|
+
// gate verifies the carried DelegationCert to K_root + binds (relation, subject) + `.delegatedFrom(parent)`.
|
|
1162
|
+
const delegationReq: Record<
|
|
1163
|
+
string,
|
|
1164
|
+
{ relation: string; subjectField: string; certField: string; parentKeyObject: string; parentKeyField: string }
|
|
1165
|
+
> = {};
|
|
1166
|
+
for (const d of [...l.module.directives].sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0))) {
|
|
1167
|
+
const g = (d as { delegatedRoleGate?: {
|
|
1168
|
+
relation: string; subjectField: string; certField: string; parentKeyObject: string; parentKeyField: string;
|
|
1169
|
+
} }).delegatedRoleGate;
|
|
1170
|
+
if (g !== undefined) {
|
|
1171
|
+
delegationReq[d.id] = {
|
|
1172
|
+
relation: g.relation,
|
|
1173
|
+
subjectField: g.subjectField,
|
|
1174
|
+
certField: g.certField,
|
|
1175
|
+
parentKeyObject: g.parentKeyObject,
|
|
1176
|
+
parentKeyField: g.parentKeyField,
|
|
1177
|
+
};
|
|
1178
|
+
}
|
|
1179
|
+
}
|
|
1180
|
+
const nomosDelegationGate =
|
|
1181
|
+
Object.keys(delegationReq).length > 0 ? { v: 1 as const, directives: delegationReq } : undefined;
|
|
1139
1182
|
// THE ATTESTED-READ GATE (slice B): per-directive `(queryId, from)` edges + the module's
|
|
1140
1183
|
// trustSource declarations. Emitted iff ≥1 directive declares a foreign read (the era
|
|
1141
1184
|
// rule). A declared `from` with NO matching trustSource is a NAMED COMPILE ERROR — the
|
|
@@ -1492,6 +1535,7 @@ export function emitUsd(
|
|
|
1492
1535
|
...(nomosReadProjectionNamespace !== undefined ? { nomosReadProjectionNamespace } : {}),
|
|
1493
1536
|
...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
|
|
1494
1537
|
...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
|
|
1538
|
+
...(nomosDelegationGate !== undefined ? { nomosDelegationGate } : {}),
|
|
1495
1539
|
...(nomosAttestedReadGate !== undefined ? { nomosAttestedReadGate } : {}),
|
|
1496
1540
|
...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
|
|
1497
1541
|
...(nomosBirthRecordGate !== undefined ? { nomosBirthRecordGate } : {}),
|
package/src/usd_layers.ts
CHANGED
|
@@ -198,6 +198,9 @@ function domainLayerAttrLines(domain: string, layer: UsdLayer, indent: string):
|
|
|
198
198
|
...(layer.nomosAttestationGate !== undefined
|
|
199
199
|
? [`${indent}custom string nomos:attestationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestationGate)))}`]
|
|
200
200
|
: []),
|
|
201
|
+
...(layer.nomosDelegationGate !== undefined
|
|
202
|
+
? [`${indent}custom string nomos:delegationGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosDelegationGate)))}`]
|
|
203
|
+
: []),
|
|
201
204
|
...(layer.nomosAttestedReadGate !== undefined
|
|
202
205
|
? [`${indent}custom string nomos:attestedReadGateHex = ${usdaString(hexUtf8(JSON.stringify(layer.nomosAttestedReadGate)))}`]
|
|
203
206
|
: []),
|
|
@@ -875,6 +878,22 @@ export function flattenLayeredUsd(layers: readonly UsdLayer[]): UsdDocument {
|
|
|
875
878
|
attestationGate = { v: 1, directives: rec };
|
|
876
879
|
}
|
|
877
880
|
}
|
|
881
|
+
// THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md): directiveId → delegation spec merged
|
|
882
|
+
// later-wins across layers, SORTED keys. Omit-when-empty (era rule).
|
|
883
|
+
let delegationGate: UsdLayer["nomosDelegationGate"];
|
|
884
|
+
{
|
|
885
|
+
type DelSrc = NonNullable<UsdLayer["nomosDelegationGate"]>["directives"][string];
|
|
886
|
+
const a = new Map<string, DelSrc>();
|
|
887
|
+
for (const l of stack) {
|
|
888
|
+
if (l.nomosDelegationGate === undefined) continue;
|
|
889
|
+
for (const [id, src] of Object.entries(l.nomosDelegationGate.directives)) a.set(id, src);
|
|
890
|
+
}
|
|
891
|
+
if (a.size > 0) {
|
|
892
|
+
const rec: Record<string, DelSrc> = {};
|
|
893
|
+
for (const k of [...a.keys()].sort()) rec[k] = a.get(k)!;
|
|
894
|
+
delegationGate = { v: 1, directives: rec };
|
|
895
|
+
}
|
|
896
|
+
}
|
|
878
897
|
// THE ATTESTED-READ GATE (slice B): sources + per-directive edges merged later-wins across
|
|
879
898
|
// layers, SORTED keys (matching the composed emission). Omit-when-empty (era rule).
|
|
880
899
|
let attestedReadGate: UsdLayer["nomosAttestedReadGate"];
|
|
@@ -1031,6 +1050,7 @@ return {
|
|
|
1031
1050
|
...(readProjectionNamespace !== undefined ? { nomosReadProjectionNamespace: readProjectionNamespace } : {}),
|
|
1032
1051
|
...(relationGate !== undefined ? { nomosRelationGate: relationGate } : {}),
|
|
1033
1052
|
...(attestationGate !== undefined ? { nomosAttestationGate: attestationGate } : {}),
|
|
1053
|
+
...(delegationGate !== undefined ? { nomosDelegationGate: delegationGate } : {}),
|
|
1034
1054
|
...(attestedReadGate !== undefined ? { nomosAttestedReadGate: attestedReadGate } : {}),
|
|
1035
1055
|
...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
|
|
1036
1056
|
...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
|
|
@@ -1173,6 +1193,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1173
1193
|
const relationNamespaceByDomain = new Map<string, UsdLayer["nomosRelationNamespace"]>();
|
|
1174
1194
|
const readProjectionNamespaceByDomain = new Map<string, UsdLayer["nomosReadProjectionNamespace"]>();
|
|
1175
1195
|
const attestationGateByDomain = new Map<string, UsdLayer["nomosAttestationGate"]>();
|
|
1196
|
+
const delegationGateByDomain = new Map<string, UsdLayer["nomosDelegationGate"]>();
|
|
1176
1197
|
const attestedReadGateByDomain = new Map<string, UsdLayer["nomosAttestedReadGate"]>();
|
|
1177
1198
|
const birthGateByDomain = new Map<string, UsdLayer["nomosBirthGate"]>();
|
|
1178
1199
|
const birthRecordGateByDomain = new Map<string, UsdLayer["nomosBirthRecordGate"]>();
|
|
@@ -1248,6 +1269,11 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1248
1269
|
if (typeof attGateHex === "string") {
|
|
1249
1270
|
attestationGateByDomain.set(domain, JSON.parse(unhexUtf8(attGateHex)) as UsdLayer["nomosAttestationGate"]);
|
|
1250
1271
|
}
|
|
1272
|
+
// THE DELEGATED-AUTHORITY GATE (delegated_authority_births.md): the hex-carried delegation spec.
|
|
1273
|
+
const delGateHex = p.attrs.get("nomos:delegationGateHex");
|
|
1274
|
+
if (typeof delGateHex === "string") {
|
|
1275
|
+
delegationGateByDomain.set(domain, JSON.parse(unhexUtf8(delGateHex)) as UsdLayer["nomosDelegationGate"]);
|
|
1276
|
+
}
|
|
1251
1277
|
// THE ATTESTED-READ GATE (slice B): the hex-carried trust-edge declaration.
|
|
1252
1278
|
const attReadGateHex = p.attrs.get("nomos:attestedReadGateHex");
|
|
1253
1279
|
if (typeof attReadGateHex === "string") {
|
|
@@ -1395,6 +1421,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1395
1421
|
const relNs = relationNamespaceByDomain.get(domain);
|
|
1396
1422
|
const rpNs = readProjectionNamespaceByDomain.get(domain);
|
|
1397
1423
|
const attGate = attestationGateByDomain.get(domain);
|
|
1424
|
+
const delGate = delegationGateByDomain.get(domain);
|
|
1398
1425
|
const attReadGate = attestedReadGateByDomain.get(domain);
|
|
1399
1426
|
const birthGate = birthGateByDomain.get(domain);
|
|
1400
1427
|
const birthRecordGate = birthRecordGateByDomain.get(domain);
|
|
@@ -1423,6 +1450,7 @@ export function parseUsdaDocument(text: string): UsdDocument {
|
|
|
1423
1450
|
...(rpNs !== undefined ? { nomosReadProjectionNamespace: rpNs } : {}),
|
|
1424
1451
|
...(relGate !== undefined ? { nomosRelationGate: relGate } : {}),
|
|
1425
1452
|
...(attGate !== undefined ? { nomosAttestationGate: attGate } : {}),
|
|
1453
|
+
...(delGate !== undefined ? { nomosDelegationGate: delGate } : {}),
|
|
1426
1454
|
...(attReadGate !== undefined ? { nomosAttestedReadGate: attReadGate } : {}),
|
|
1427
1455
|
...(birthGate !== undefined ? { nomosBirthGate: birthGate } : {}),
|
|
1428
1456
|
...(birthRecordGate !== undefined ? { nomosBirthRecordGate: birthRecordGate } : {}),
|