@githolon/dsl 0.33.0 → 0.34.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/package.json +1 -1
- package/src/aggregate.ts +12 -1
- package/src/authoring.ts +4 -1
- package/src/birth_meter.ts +300 -0
- package/src/birth_meter_ensure.ts +67 -0
- package/src/build_package.ts +34 -0
- package/src/codegen_dart.ts +9 -5
- package/src/codegen_proof.ts +13 -7
- package/src/compile_engine.ts +13 -2
- package/src/compile_package_main.ts +28 -1
- package/src/directive.ts +10 -1
- package/src/engine_entry.ts +56 -9
- package/src/framework/birth_meter.ts +125 -0
- package/src/framework/impure_capability.ts +5 -2
- package/src/framework/platform_admin.ts +4 -1
- package/src/framework/relations.ts +19 -0
- package/src/framework/workspaces.ts +50 -4
- package/src/index.ts +34 -0
- package/src/read.ts +22 -0
- package/src/read_projection_composition.ts +117 -0
- package/src/read_projection_manifest.ts +94 -0
- package/src/read_projection_namespace.ts +152 -0
- package/src/usd.ts +121 -11
- package/src/usd_layers.ts +71 -0
- package/src/value_object.ts +43 -0
- package/src/wire_encode.ts +34 -3
|
@@ -0,0 +1,117 @@
|
|
|
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 · SQLite projection · embedded QuickJS engine}.
|
|
4
|
+
//
|
|
5
|
+
// COMPILE-TIME DIVERGENCE GUARD for SHARED read-projection keys — the read-layer
|
|
6
|
+
// sibling of `relation_composition.ts` (the scalpel).
|
|
7
|
+
//
|
|
8
|
+
// THE MODEL. The kernel's installed-read-manifest accumulator
|
|
9
|
+
// (`wasm_git_holon.rs::ReadProjectionAccumulator`) UNIONS every active install's
|
|
10
|
+
// `nomosReadProjection` fragment BY KEY — `aggregateFieldKinds` by aggregate TYPE,
|
|
11
|
+
// `queries`/`counts`/`spatials`/`sums`/`mins`/`maxes`/`orderedReads` by declared `id`,
|
|
12
|
+
// `deriveds`/`combineds` by `"<of>\0<id>"` — and (correctly, fail-closed) REFUSES a
|
|
13
|
+
// key declared twice with DIVERGENT shapes: "divergent read projection declaration
|
|
14
|
+
// for key <k>". At RUNTIME that refusal WEDGES the workspace's read plane.
|
|
15
|
+
//
|
|
16
|
+
// Read-projection NAMESPACING (`read_projection_namespace.ts`) makes a NON-GLOBAL
|
|
17
|
+
// aggregate's keys domain-qualified, so tenant domains never share a key. What CAN
|
|
18
|
+
// still share a key inside one package: GLOBAL framework aggregates declared by ≥2
|
|
19
|
+
// domains (byte-identical by construction — same framework version — but a version
|
|
20
|
+
// skew diverges), and same-named declared reads across domains. This guard turns the
|
|
21
|
+
// runtime wedge into a precise COMPILE refusal: identical shared declarations PASS
|
|
22
|
+
// (the kernel dedupes them), divergent ones REFUSE with the key and each domain's
|
|
23
|
+
// shape. Validation only — a conflict-free package is returned untouched (era rule).
|
|
24
|
+
|
|
25
|
+
import type { UsdLayer } from "./usd.js";
|
|
26
|
+
import { domainOfLayerPath } from "./relation_composition.js";
|
|
27
|
+
|
|
28
|
+
/** One shared-key divergence, gathered for the scalpel message. */
|
|
29
|
+
interface ReadProjectionConflict {
|
|
30
|
+
/** The accumulator section (`aggregateFieldKinds`, `queries`, …). */
|
|
31
|
+
readonly section: string;
|
|
32
|
+
/** The colliding accumulator key (the kernel's exact key). */
|
|
33
|
+
readonly key: string;
|
|
34
|
+
/** Each contributing domain's declaration, in package (weak→strong) order. */
|
|
35
|
+
readonly opinions: { readonly domain: string; readonly shape: string }[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function scalpelMessage(conflicts: readonly ReadProjectionConflict[]): string {
|
|
39
|
+
const lines: string[] = [
|
|
40
|
+
`Refusing to compose the package: shared read-projection key(s) carry DIVERGENT declarations ` +
|
|
41
|
+
`across domains — the kernel's installed-read-manifest accumulator refuses exactly this at ` +
|
|
42
|
+
`SERVE time ("divergent read projection declaration for key …"), wedging the workspace's ` +
|
|
43
|
+
`read plane. Fail-closed compile refusal, not a runtime wedge.`,
|
|
44
|
+
``,
|
|
45
|
+
];
|
|
46
|
+
for (const c of conflicts) {
|
|
47
|
+
lines.push(
|
|
48
|
+
` read projection "${c.section}" key "${c.key.replace("\0", " · ")}" — ${c.opinions.length} divergent declarations:`,
|
|
49
|
+
);
|
|
50
|
+
const w = Math.max(...c.opinions.map((o) => o.domain.length));
|
|
51
|
+
for (const o of c.opinions) lines.push(` · ${o.domain.padEnd(w)} ${o.shape}`);
|
|
52
|
+
}
|
|
53
|
+
lines.push(
|
|
54
|
+
``,
|
|
55
|
+
`Fix: give the shared key ONE canonical declaration (the domains must agree, byte-for-byte — ` +
|
|
56
|
+
`for a framework aggregate that means the SAME framework version in every domain), or make ` +
|
|
57
|
+
`the declarations DISTINCT (a non-global aggregate auto-qualifies by its owning domain; a ` +
|
|
58
|
+
`declared read can simply take a different id).`,
|
|
59
|
+
);
|
|
60
|
+
return lines.join("\n");
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
type Fragment = NonNullable<UsdLayer["nomosReadProjection"]>;
|
|
64
|
+
|
|
65
|
+
/** The accumulator sections keyed by declared `id` (mirrors `merge_projection_array_by_id`). */
|
|
66
|
+
const BY_ID_SECTIONS = ["queries", "counts", "spatials", "sums", "mins", "maxes", "orderedReads"] as const;
|
|
67
|
+
/** The sections keyed by `"<of>\0<id>"` (mirrors `merge_projection_array_by_type_and_id`). */
|
|
68
|
+
const BY_TYPE_AND_ID_SECTIONS = ["deriveds", "combineds"] as const;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Guard the package's layers against a shared-read-projection-key divergence — the EXACT key
|
|
72
|
+
* semantics the kernel accumulator merges by. Identical shared declarations pass (dedupe);
|
|
73
|
+
* divergent ones throw the scalpel. Deterministic (sorted keys/sections). Returns the layers
|
|
74
|
+
* UNTOUCHED (validation only).
|
|
75
|
+
*
|
|
76
|
+
* @throws a scalpel `Error` naming the section, key, and each domain's declaration on divergence.
|
|
77
|
+
*/
|
|
78
|
+
export function guardSharedReadProjectionKeys(layers: readonly UsdLayer[]): readonly UsdLayer[] {
|
|
79
|
+
// section → key → the contributing {domain, canonical JSON} opinions, in package order.
|
|
80
|
+
const seen = new Map<string, Map<string, { domain: string; shape: string }[]>>();
|
|
81
|
+
const note = (section: string, key: string, domain: string, value: unknown): void => {
|
|
82
|
+
const bySection = seen.get(section) ?? new Map<string, { domain: string; shape: string }[]>();
|
|
83
|
+
seen.set(section, bySection);
|
|
84
|
+
const arr = bySection.get(key) ?? [];
|
|
85
|
+
arr.push({ domain, shape: JSON.stringify(value) });
|
|
86
|
+
bySection.set(key, arr);
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
for (const layer of layers) {
|
|
90
|
+
const frag: Fragment | undefined = layer.nomosReadProjection;
|
|
91
|
+
if (frag === undefined) continue;
|
|
92
|
+
const domain = domainOfLayerPath(layer.path);
|
|
93
|
+
for (const [ty, schema] of Object.entries(frag.aggregateFieldKinds ?? {})) {
|
|
94
|
+
note("aggregateFieldKinds", ty, domain, schema);
|
|
95
|
+
}
|
|
96
|
+
const bag = frag as unknown as Record<string, readonly { id: string; of?: string }[] | undefined>;
|
|
97
|
+
for (const section of BY_ID_SECTIONS) {
|
|
98
|
+
for (const item of bag[section] ?? []) note(section, item.id, domain, item);
|
|
99
|
+
}
|
|
100
|
+
for (const section of BY_TYPE_AND_ID_SECTIONS) {
|
|
101
|
+
for (const item of bag[section] ?? []) note(section, `${item.of}\0${item.id}`, domain, item);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
const conflicts: ReadProjectionConflict[] = [];
|
|
106
|
+
for (const section of [...seen.keys()].sort()) {
|
|
107
|
+
const bySection = seen.get(section)!;
|
|
108
|
+
for (const key of [...bySection.keys()].sort()) {
|
|
109
|
+
const opinions = bySection.get(key)!;
|
|
110
|
+
if (opinions.length < 2) continue;
|
|
111
|
+
const distinct = new Set(opinions.map((o) => o.shape));
|
|
112
|
+
if (distinct.size > 1) conflicts.push({ section, key, opinions });
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
if (conflicts.length > 0) throw new Error(scalpelMessage(conflicts));
|
|
116
|
+
return layers;
|
|
117
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
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 · SQLite projection · embedded QuickJS engine}.
|
|
4
|
+
//
|
|
5
|
+
// READ-PROJECTION NAMESPACING — the CANONICAL-MANIFEST label transform (the compile-side
|
|
6
|
+
// rename). BUILD-TIME ONLY, split out of `read_projection_namespace.ts` deliberately: this
|
|
7
|
+
// file types against `manifest.ts` (which imports `node:crypto`/`Buffer` for the domain
|
|
8
|
+
// hash), and the classification/qualifier half is imported by PLATFORM-NEUTRAL engine
|
|
9
|
+
// files (`engine_entry.ts`, `wire_encode.ts`, `framework/relations.ts`) that tenant
|
|
10
|
+
// scaffolds typecheck WITHOUT `@types/node`. Keeping the manifest edge here (reached only
|
|
11
|
+
// via `usd.ts`, the build lane) keeps the neutral type graph node-free — the same
|
|
12
|
+
// build/runtime split `stable_ids.ts` vs `stable_ids_types.ts` ratified.
|
|
13
|
+
|
|
14
|
+
import type { CanonicalManifest } from "./manifest.js";
|
|
15
|
+
import type { StableIds, StableAggregateId } from "./stable_ids_types.js";
|
|
16
|
+
import { qualifyReadProjectionType } from "./read_projection_namespace.js";
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* Qualify EVERY aggregate-type label a domain's canonical manifest carries — exactly as
|
|
20
|
+
* if the author had renamed `Entry` → `<domainKey>__Entry` in source, which is the #58
|
|
21
|
+
* rename the whole system already supports (sid identity unmoved; the projection folds
|
|
22
|
+
* old-label rows onto the new label via the `was` lineage). Touches: aggregate ids,
|
|
23
|
+
* directive targets, `queries[].returns`, every maintained read's `of` (counts / sums /
|
|
24
|
+
* mins / maxes / spatials / deriveds / combineds / orderedReads), `combineds[].reads`
|
|
25
|
+
* (a related aggregate TYPE — the cross-aggregate invalidation key), and the stable-ids
|
|
26
|
+
* labels (the BARE name is APPENDED to the sid's `was` lineage so pre-namespacing rows
|
|
27
|
+
* normalize onto the qualified label). Fields keep their bare labels (field names never
|
|
28
|
+
* collide across types — the accumulator keys them under the type). Pure + deterministic;
|
|
29
|
+
* with an empty `namespaced` set the manifest is returned UNTOUCHED (the era rule).
|
|
30
|
+
*/
|
|
31
|
+
export function qualifyManifestAggregateLabels(
|
|
32
|
+
manifest: CanonicalManifest,
|
|
33
|
+
domainKey: string,
|
|
34
|
+
namespaced: ReadonlySet<string>,
|
|
35
|
+
): CanonicalManifest {
|
|
36
|
+
if (namespaced.size === 0) return manifest;
|
|
37
|
+
const qa = (n: string): string => (namespaced.has(n) ? qualifyReadProjectionType(domainKey, n) : n);
|
|
38
|
+
|
|
39
|
+
const stableIds: StableIds | undefined =
|
|
40
|
+
manifest.stableIds === undefined
|
|
41
|
+
? undefined
|
|
42
|
+
: {
|
|
43
|
+
...manifest.stableIds,
|
|
44
|
+
aggregates: Object.fromEntries(
|
|
45
|
+
Object.entries(manifest.stableIds.aggregates).map(([name, rec]): [string, StableAggregateId] => {
|
|
46
|
+
if (!namespaced.has(name)) return [name, rec];
|
|
47
|
+
// The bare labels this sid's rows may carry: every prior bare label + the current
|
|
48
|
+
// bare name (pre-namespacing rows; the runtime stamp is qualified going forward).
|
|
49
|
+
const was = [...new Set([...(rec.was ?? []), name])];
|
|
50
|
+
return [qa(name), { ...rec, was }];
|
|
51
|
+
}),
|
|
52
|
+
),
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
return {
|
|
56
|
+
...manifest,
|
|
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)),
|
|
59
|
+
...(stableIds !== undefined ? { stableIds } : {}),
|
|
60
|
+
...(manifest.queries !== undefined
|
|
61
|
+
? { queries: manifest.queries.map((q) => (namespaced.has(q.returns) ? { ...q, returns: qa(q.returns) } : q)) }
|
|
62
|
+
: {}),
|
|
63
|
+
...(manifest.counts !== undefined
|
|
64
|
+
? { counts: manifest.counts.map((c) => (namespaced.has(c.of) ? { ...c, of: qa(c.of) } : c)) }
|
|
65
|
+
: {}),
|
|
66
|
+
...(manifest.sums !== undefined
|
|
67
|
+
? { sums: manifest.sums.map((s) => (namespaced.has(s.of) ? { ...s, of: qa(s.of) } : s)) }
|
|
68
|
+
: {}),
|
|
69
|
+
...(manifest.mins !== undefined
|
|
70
|
+
? { mins: manifest.mins.map((e) => (namespaced.has(e.of) ? { ...e, of: qa(e.of) } : e)) }
|
|
71
|
+
: {}),
|
|
72
|
+
...(manifest.maxes !== undefined
|
|
73
|
+
? { maxes: manifest.maxes.map((e) => (namespaced.has(e.of) ? { ...e, of: qa(e.of) } : e)) }
|
|
74
|
+
: {}),
|
|
75
|
+
...(manifest.spatials !== undefined
|
|
76
|
+
? { spatials: manifest.spatials.map((s) => (namespaced.has(s.of) ? { ...s, of: qa(s.of) } : s)) }
|
|
77
|
+
: {}),
|
|
78
|
+
...(manifest.deriveds !== undefined
|
|
79
|
+
? { deriveds: manifest.deriveds.map((d) => (namespaced.has(d.of) ? { ...d, of: qa(d.of) } : d)) }
|
|
80
|
+
: {}),
|
|
81
|
+
...(manifest.combineds !== undefined
|
|
82
|
+
? {
|
|
83
|
+
combineds: manifest.combineds.map((c) => ({
|
|
84
|
+
...c,
|
|
85
|
+
...(namespaced.has(c.of) ? { of: qa(c.of) } : {}),
|
|
86
|
+
...(namespaced.has(c.reads) ? { reads: qa(c.reads) } : {}),
|
|
87
|
+
})),
|
|
88
|
+
}
|
|
89
|
+
: {}),
|
|
90
|
+
...(manifest.orderedReads !== undefined
|
|
91
|
+
? { orderedReads: manifest.orderedReads.map((r) => (namespaced.has(r.of) ? { ...r, of: qa(r.of) } : r)) }
|
|
92
|
+
: {}),
|
|
93
|
+
};
|
|
94
|
+
}
|
|
@@ -0,0 +1,152 @@
|
|
|
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 · SQLite projection · embedded QuickJS engine}.
|
|
4
|
+
//
|
|
5
|
+
// READ-PROJECTION (aggregate-type) NAMESPACING — the read-layer mirror of the shipped
|
|
6
|
+
// relation-type namespacing (`relation_schema.ts::qualifyRelationType`).
|
|
7
|
+
//
|
|
8
|
+
// THE DISEASE. The kernel's installed-read-manifest accumulator
|
|
9
|
+
// (`wasm_git_holon.rs::insert_read_projection_value`) unions every ACTIVE install's
|
|
10
|
+
// `nomosReadProjection` fragment and — correctly, fail-closed — REFUSES when two
|
|
11
|
+
// installs declare the SAME key with DIVERGENT shapes ("divergent read projection
|
|
12
|
+
// declaration for key AuthProvider"). Two different domain keys that each declare a
|
|
13
|
+
// same-named aggregate therefore WEDGE the workspace's read plane at serve time.
|
|
14
|
+
//
|
|
15
|
+
// THE CURE (the maintainer's rule: "Namespaces are a good idea. Use them."): a
|
|
16
|
+
// NON-GLOBAL aggregate's TYPE NAME is QUALIFIED by its owning domain key at compile —
|
|
17
|
+
// `Entry` in domain `guestbook` becomes `guestbook.Entry` — so two domains that each
|
|
18
|
+
// declare the same bare name NEVER share an accumulator key. The delimiter is `.`
|
|
19
|
+
// (NOT relations' `__`: the id-mint gate splits a minted id's type tag on the first
|
|
20
|
+
// `_`, and the type must also stay colon-free — see READ_PROJECTION_DELIMITER). The engine stamps the SAME qualified name onto every event's
|
|
21
|
+
// `__type` at plan runtime (see `wire_encode.ts`), so the projected row's type and the
|
|
22
|
+
// installed schema key agree by construction; the BARE name rides the aggregate's
|
|
23
|
+
// stable-id `was` lineage so pre-namespacing rows normalize onto the qualified label
|
|
24
|
+
// (the #58 rename machinery — a qualification IS a rename).
|
|
25
|
+
//
|
|
26
|
+
// GLOBAL (un-namespaced) aggregates — the opt-out. Framework aggregates the KERNEL
|
|
27
|
+
// reads by bare name / bare instance-id convention (AuthProvider for in-kernel JWT
|
|
28
|
+
// verification, RelationTuple for the native authz fold, the domain-lifecycle law
|
|
29
|
+
// records, …) MUST keep their bare identity. They are enumerated DELIBERATELY in the
|
|
30
|
+
// explicit allowlist below (never a heuristic); a drift test pins the list to the
|
|
31
|
+
// real framework exports. A dynamically-named framework aggregate (a factory product,
|
|
32
|
+
// e.g. `platformAdmin()`'s founder aggregate) opts out via `aggregate(id, fields,
|
|
33
|
+
// { global: true })` instead — the flag rides the handle into BOTH the compiler and
|
|
34
|
+
// the sealed engine, so the two classifications can never disagree.
|
|
35
|
+
//
|
|
36
|
+
// ERA RULE (the law is the era): qualification happens at COMPILE — already-deployed
|
|
37
|
+
// packages are byte-identical untouched, and a qualified key is just a NEW name to the
|
|
38
|
+
// kernel accumulator (no kernel change). The `nomosReadProjectionNamespace` era key is
|
|
39
|
+
// emitted IFF a layer qualified ≥1 aggregate — the marker that this bundle's read keys
|
|
40
|
+
// are domain-qualified (mirrors `nomosRelationNamespace`).
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* The read-projection (aggregate-type) qualifier delimiter. NOT the relation namespacing's
|
|
44
|
+
* `__`, deliberately: an aggregate type is also the kernel id-mint TYPE TAG
|
|
45
|
+
* (`id-mint/src/lib.rs::type_tag_of` splits a minted id on the FIRST `_`), so an
|
|
46
|
+
* underscore-bearing type can never mint — `guestbook__Entry_uuid` parses as tag
|
|
47
|
+
* `guestbook` and refuses at the id-mint gate. And it must stay colon-free (`ObjectRef::
|
|
48
|
+
* parse` splits addresses on the first `:`). `.` satisfies both parsers. The ONE surface
|
|
49
|
+
* where `.` is illegal — a real-USD PRIM NAME (identifiers only) — is already covered by the
|
|
50
|
+
* `.usda` text lane's `primName()` sanitizer (`usd_layers.ts`), which round-trips the raw id
|
|
51
|
+
* via `nomos:id`; the TYPE STRING everywhere (prim `type` attr, directive `target`,
|
|
52
|
+
* read-projection keys, the runtime `__type` stamp, stable-id labels, the mint tag) is dotted.
|
|
53
|
+
*/
|
|
54
|
+
export const READ_PROJECTION_DELIMITER = ".";
|
|
55
|
+
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* THE EXPLICIT GLOBAL ALLOWLIST — every STATICALLY-NAMED framework aggregate. These
|
|
59
|
+
* stay BARE on every surface (read projection, prims, stable-ids, runtime `__type`)
|
|
60
|
+
* because the kernel and the framework machinery address them by bare name /
|
|
61
|
+
* bare-name-derived instance ids (`auth-provider:platform`, the relation fold, the
|
|
62
|
+
* policy/lifecycle law records, the signer spine, the key-fanout gate, …).
|
|
63
|
+
*
|
|
64
|
+
* DELIBERATE ENUMERATION, not a heuristic: `read_projection_namespace.test.ts` pins
|
|
65
|
+
* this set to the real `FRAMEWORK_AGGREGATE_IDS` registry so it can never drift.
|
|
66
|
+
* (A tenant aggregate that merely REUSES one of these names also stays bare — that
|
|
67
|
+
* matches the kernel-couples-by-name reality and is exactly the shared-key case the
|
|
68
|
+
* compile-time divergence guard `read_projection_composition.ts` polices.)
|
|
69
|
+
*/
|
|
70
|
+
export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set([
|
|
71
|
+
// disclosure
|
|
72
|
+
"Disclosure",
|
|
73
|
+
// domain lifecycle (the law records themselves)
|
|
74
|
+
"PolicyBundle",
|
|
75
|
+
"DomainInstallation",
|
|
76
|
+
// encryption identity + scope (the operator-blind key machinery)
|
|
77
|
+
"EncKey",
|
|
78
|
+
"KeyScope",
|
|
79
|
+
"WrappedKey",
|
|
80
|
+
// genesis / births
|
|
81
|
+
"GenesisWarrant",
|
|
82
|
+
// metered birth (slice A) — NomosBirthRecord is kernel-read by the birth-record
|
|
83
|
+
// gate (the `nomos-birth-record:` prefix); the policy/grant singletons ride with it
|
|
84
|
+
"NomosBirthRecord",
|
|
85
|
+
"BirthPolicy",
|
|
86
|
+
"BirthGrant",
|
|
87
|
+
// home holons
|
|
88
|
+
"HomeOwner",
|
|
89
|
+
"SharedWithMe",
|
|
90
|
+
"BirthAttestation",
|
|
91
|
+
// identity
|
|
92
|
+
"Actor",
|
|
93
|
+
"User",
|
|
94
|
+
"DeviceKey",
|
|
95
|
+
"Organization",
|
|
96
|
+
"PublicUser",
|
|
97
|
+
// native relations (the one authz spine)
|
|
98
|
+
"RelationTuple",
|
|
99
|
+
// repair
|
|
100
|
+
"RepairRecord",
|
|
101
|
+
// signer spine
|
|
102
|
+
"SignerEpoch",
|
|
103
|
+
"SignerDelegation",
|
|
104
|
+
// governance / workspaces
|
|
105
|
+
"WorkspaceSelf",
|
|
106
|
+
"CloudWorkspace",
|
|
107
|
+
"CloudPolicy",
|
|
108
|
+
"QuotaGrant",
|
|
109
|
+
"PlatformGrant",
|
|
110
|
+
"CapabilityBinding",
|
|
111
|
+
"AuthProvider",
|
|
112
|
+
"HostnameClaim",
|
|
113
|
+
]);
|
|
114
|
+
|
|
115
|
+
/** The minimal handle shape the classification reads (compiler AND engine — one function). */
|
|
116
|
+
export interface NamespaceClassifiable {
|
|
117
|
+
readonly id: string;
|
|
118
|
+
/** The `aggregate(id, fields, { global: true })` opt-out — kernel-coupled dynamic framework aggregates. */
|
|
119
|
+
readonly global?: boolean;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* TRUE iff this aggregate's type name is NAMESPACED (qualified by its owning domain).
|
|
124
|
+
* FALSE for the explicit framework allowlist and for `{ global: true }` handles.
|
|
125
|
+
* ONE classification, consumed by the compiler (USD lowering) and the sealed engine
|
|
126
|
+
* (the runtime `__type` stamp) — they can never disagree.
|
|
127
|
+
*/
|
|
128
|
+
export function isNamespacedAggregate(agg: NamespaceClassifiable): boolean {
|
|
129
|
+
return agg.global !== true && !GLOBAL_READ_PROJECTION_AGGREGATE_IDS.has(agg.id);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Qualify a bare aggregate type by its owning domain key: `("guestbook","Entry") → "guestbook.Entry"`.
|
|
134
|
+
* The domain-key half is MINT-TAG-SAFE-encoded (`_` → `-`): the qualified type is also the kernel
|
|
135
|
+
* id-mint TYPE TAG, and `type_tag_of` splits a minted id on the FIRST `_` — so NO underscore may
|
|
136
|
+
* appear anywhere in the tag (`co2_platform.X` would parse as tag `co2`). Deterministic; the era
|
|
137
|
+
* key witnesses the encoded form.
|
|
138
|
+
*/
|
|
139
|
+
export function qualifyReadProjectionType(domainKey: string, type: string): string {
|
|
140
|
+
return `${domainKey.replace(/_/g, "-")}${READ_PROJECTION_DELIMITER}${type}`;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* The NAMESPACED (non-global) aggregate ids a module declares — the set whose type
|
|
145
|
+
* names qualify. Derived from the live handles (the `global` flag rides the handle),
|
|
146
|
+
* shared by `usd.ts` (compile) and `engine_entry.ts` (the dispatch context).
|
|
147
|
+
*/
|
|
148
|
+
export function namespacedAggregateIds(aggregates: readonly NamespaceClassifiable[]): Set<string> {
|
|
149
|
+
const out = new Set<string>();
|
|
150
|
+
for (const a of aggregates) if (isNamespacedAggregate(a)) out.add(a.id);
|
|
151
|
+
return out;
|
|
152
|
+
}
|
package/src/usd.ts
CHANGED
|
@@ -45,6 +45,14 @@ import { domainInterface } from "./shape_compat.js";
|
|
|
45
45
|
import { relationType, user, key, anyUser, qualifiedTypeId, qualifyRelationType } from "./relation_schema.js";
|
|
46
46
|
import { harvestPrincipalFlows } from "./wire_encode.js";
|
|
47
47
|
import { composeSharedRelationTypes } from "./relation_composition.js";
|
|
48
|
+
import { collectValueObjectUsages } from "./value_object.js";
|
|
49
|
+
import {
|
|
50
|
+
namespacedAggregateIds,
|
|
51
|
+
qualifyReadProjectionType,
|
|
52
|
+
READ_PROJECTION_DELIMITER,
|
|
53
|
+
} from "./read_projection_namespace.js";
|
|
54
|
+
import { qualifyManifestAggregateLabels } from "./read_projection_manifest.js";
|
|
55
|
+
import { guardSharedReadProjectionKeys } from "./read_projection_composition.js";
|
|
48
56
|
|
|
49
57
|
/** The referential marker of a directive prim (kernel `RefMode` wire string). */
|
|
50
58
|
export type RefMode = WireRefMode;
|
|
@@ -276,6 +284,22 @@ export interface UsdLayer {
|
|
|
276
284
|
readonly delimiter: string;
|
|
277
285
|
readonly types: readonly string[];
|
|
278
286
|
};
|
|
287
|
+
/**
|
|
288
|
+
* THE READ-PROJECTION NAMESPACE ERA KEY (read-projection namespacing — the read-layer mirror of
|
|
289
|
+
* `nomosRelationNamespace`). Present IFF this layer qualified ≥1 (non-global) AGGREGATE TYPE by its
|
|
290
|
+
* owning domain key (`Entry` → `guestbook__Entry`), so two domains / two installs that each declare
|
|
291
|
+
* the same bare aggregate name no longer collide at the kernel's installed-read-manifest accumulator
|
|
292
|
+
* ("divergent read projection declaration for key …" — the fail-closed union stays the trust
|
|
293
|
+
* boundary; a qualified key is simply a NEW name to it). `types` is the SORTED set of qualified type
|
|
294
|
+
* names this layer produced; `delimiter` is the same ratified colon-free `"__"`. OMITTED for a layer
|
|
295
|
+
* whose aggregates are all GLOBAL (the explicit framework allowlist / `{global:true}`) — the marker
|
|
296
|
+
* is a witness; the qualification itself is baked into the layer's keys.
|
|
297
|
+
*/
|
|
298
|
+
readonly nomosReadProjectionNamespace?: {
|
|
299
|
+
readonly v: 1;
|
|
300
|
+
readonly delimiter: string;
|
|
301
|
+
readonly types: readonly string[];
|
|
302
|
+
};
|
|
279
303
|
/**
|
|
280
304
|
* THE CROSS-CUSTODY ATTESTATION GATE (home_holons §3/§5). Present IFF ≥1 directive declares
|
|
281
305
|
* `.attestationAuthorized()`: the SORTED set of their ids. A directive named here is authorized by a
|
|
@@ -339,6 +363,25 @@ export interface UsdLayer {
|
|
|
339
363
|
/** The directive ids declared `.births()` — SORTED. */
|
|
340
364
|
readonly directives: string[];
|
|
341
365
|
};
|
|
366
|
+
/**
|
|
367
|
+
* THE BIRTH-RECORD GATE (metered birth, slice A1 — `architecture/metered_birth_and_attested_reads.md`).
|
|
368
|
+
* Present IFF the layer declares ≥1 `.births()` directive AND was compiled under the metered-birth
|
|
369
|
+
* compiler: the law's EXPLICIT declaration that every birth it performs ALSO writes a structural
|
|
370
|
+
* `NomosBirthRecord` event (deterministic id `<prefix><child>`) in the SAME intent as the birth
|
|
371
|
+
* effect — the `.births()` lane appends it automatically, so the declaration is self-fulfilling for
|
|
372
|
+
* honestly-compiled law and the kernel arm only ever bites a forged/hand-built lump. The kernel's
|
|
373
|
+
* `verify_births_witness` reads THIS declaration (the SAME way it reads `nomosBirthGate` — the
|
|
374
|
+
* bundle declares, the kernel honors; NO kernel version-if) and refuses a new-era birth whose
|
|
375
|
+
* re-derived events carry no record (`birth-record-missing`). OMITTED for pre-feature law — the
|
|
376
|
+
* era rule: every chain sealed before this key replays byte-identical, births exactly as today.
|
|
377
|
+
*/
|
|
378
|
+
readonly nomosBirthRecordGate?: {
|
|
379
|
+
readonly v: 1;
|
|
380
|
+
/** The record aggregate TYPE name (the framework's `NomosBirthRecord`). */
|
|
381
|
+
readonly aggregate: string;
|
|
382
|
+
/** The deterministic instance-id prefix — the record for child `w` lives at `<prefix><w>`. */
|
|
383
|
+
readonly prefix: string;
|
|
384
|
+
};
|
|
342
385
|
/**
|
|
343
386
|
* THE PEER-SOVEREIGN ROOT-OF-TRUST (the BirthAttestation cert chain). Present IFF the law declares
|
|
344
387
|
* `requireParentAttestedBirth`: a warranted workspace running this law MUST travel with a SIGNED cert
|
|
@@ -476,6 +519,14 @@ export interface UsdLayer {
|
|
|
476
519
|
readonly nomosValueObjects?: {
|
|
477
520
|
readonly v: 1;
|
|
478
521
|
readonly valueObjects: readonly import("./fields.js").ValueObjectDescriptor[];
|
|
522
|
+
/**
|
|
523
|
+
* THE VO↔FIELD ATTACHMENT (additive, hash-inert): which aggregate FIELD carries
|
|
524
|
+
* which VO — `{aggregate, field, vo}` sorted by aggregate then field. Rides THIS
|
|
525
|
+
* key (never the aggregate prim's wire schema) so the canonical manifest/domainHash
|
|
526
|
+
* of a VO field stays byte-equal to a plain `t.jsonObject()` field. Optional for
|
|
527
|
+
* era tolerance: a 0.33.0-compiled package's key parses unchanged without it.
|
|
528
|
+
*/
|
|
529
|
+
readonly usages?: readonly import("./value_object.js").ValueObjectUsage[];
|
|
479
530
|
};
|
|
480
531
|
}
|
|
481
532
|
|
|
@@ -499,13 +550,15 @@ function sortPrims(prims: UsdPrim[]): UsdPrim[] {
|
|
|
499
550
|
* encoded schema; directive prims map `requiresCapability` → `requires: [cap]`, leave
|
|
500
551
|
* `scope`/`payloadRef` `undefined` (no static scope today; payload is stage 2).
|
|
501
552
|
*/
|
|
502
|
-
function encodeModuleToPrims(layerPath: string,
|
|
503
|
-
const manifest = domainManifest(mod);
|
|
553
|
+
function encodeModuleToPrims(layerPath: string, manifest: CanonicalManifest): UsdPrim[] {
|
|
504
554
|
const prims: UsdPrim[] = [];
|
|
505
555
|
|
|
506
556
|
for (const agg of manifest.aggregates) {
|
|
507
557
|
prims.push({
|
|
508
558
|
kind: "aggregate",
|
|
559
|
+
// The IR prim path carries the (possibly dotted-qualified) type verbatim; the `.usda`
|
|
560
|
+
// TEXT lane sanitizes the prim NAME (`usd_layers.ts::primName`) and round-trips the raw
|
|
561
|
+
// id via `nomos:id`, so real-USD identifier rules are already satisfied downstream.
|
|
509
562
|
path: `${layerPath}/${agg.id}`,
|
|
510
563
|
type: agg.id,
|
|
511
564
|
schema: agg.schema,
|
|
@@ -538,6 +591,7 @@ function encodeModuleToPrims(layerPath: string, mod: DomainModule): UsdPrim[] {
|
|
|
538
591
|
|
|
539
592
|
function aggregateFieldKindsForModule(
|
|
540
593
|
mod: DomainModule,
|
|
594
|
+
qa: (type: string) => string = (n) => n,
|
|
541
595
|
): Record<string, Record<string, UsdFieldKindDescriptor>> {
|
|
542
596
|
const out: Record<string, Record<string, UsdFieldKindDescriptor>> = {};
|
|
543
597
|
for (const agg of [...(mod.aggregates as AggregateHandle[])].sort((a, b) =>
|
|
@@ -551,7 +605,7 @@ function aggregateFieldKindsForModule(
|
|
|
551
605
|
...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
|
|
552
606
|
};
|
|
553
607
|
}
|
|
554
|
-
out[agg.id] = fields;
|
|
608
|
+
out[qa(agg.id)] = fields;
|
|
555
609
|
}
|
|
556
610
|
return out;
|
|
557
611
|
}
|
|
@@ -578,8 +632,12 @@ function readKeyFieldKind(
|
|
|
578
632
|
return descriptor.kind;
|
|
579
633
|
}
|
|
580
634
|
|
|
581
|
-
function readProjectionForModule(
|
|
582
|
-
|
|
635
|
+
function readProjectionForModule(
|
|
636
|
+
mod: DomainModule,
|
|
637
|
+
manifest: CanonicalManifest,
|
|
638
|
+
qa: (type: string) => string = (n) => n,
|
|
639
|
+
): UsdReadProjection {
|
|
640
|
+
const aggregateFieldKinds = aggregateFieldKindsForModule(mod, qa);
|
|
583
641
|
return {
|
|
584
642
|
aggregateFieldKinds,
|
|
585
643
|
queries: (manifest.queries ?? []).map((q) => ({
|
|
@@ -743,13 +801,35 @@ export function emitUsd(
|
|
|
743
801
|
// Carry the domain's declared queries straight from the canonical manifest,
|
|
744
802
|
// which already applies omit-when-empty: `queries` is present here ONLY when the
|
|
745
803
|
// module declared them, so a query-free layer is byte-identical to before.
|
|
746
|
-
const
|
|
804
|
+
const bareManifest = domainManifest(l.module);
|
|
805
|
+
// READ-PROJECTION NAMESPACING: every NON-GLOBAL aggregate's type name is qualified by the
|
|
806
|
+
// owning domain key — on the prims, the identity manifest, the stable-ids labels (bare name
|
|
807
|
+
// appended to `was` — the rename lineage), the read projection, and every `of`/`returns`
|
|
808
|
+
// reference — so two domains / two installs that each declare the same bare name never share
|
|
809
|
+
// an installed-read-manifest accumulator key. GLOBAL (framework allowlist / `{global:true}`)
|
|
810
|
+
// aggregates stay bare (the kernel addresses them by bare name). The engine stamps the SAME
|
|
811
|
+
// qualified name at plan runtime (`wire_encode.ts` via the dispatch context), so rows and
|
|
812
|
+
// schema keys agree by construction. `module.domain ?? module.name` — the SAME qualifier key
|
|
813
|
+
// relation namespacing uses (=== `intent.domain` at plan runtime).
|
|
814
|
+
const rpDomainKey = l.module.domain ?? l.module.name;
|
|
815
|
+
const namespacedAggs = namespacedAggregateIds(
|
|
816
|
+
l.module.aggregates as unknown as readonly { id: string; global?: boolean }[],
|
|
817
|
+
);
|
|
818
|
+
const qaType = (n: string): string =>
|
|
819
|
+
namespacedAggs.has(n) ? qualifyReadProjectionType(rpDomainKey, n) : n;
|
|
820
|
+
const manifest = qualifyManifestAggregateLabels(bareManifest, rpDomainKey, namespacedAggs);
|
|
821
|
+
// THE READ-PROJECTION NAMESPACE ERA KEY: present iff ≥1 aggregate was qualified (the witness
|
|
822
|
+
// marker — mirrors `nomosRelationNamespace`; the qualification itself is baked into the keys).
|
|
823
|
+
const nomosReadProjectionNamespace =
|
|
824
|
+
namespacedAggs.size > 0
|
|
825
|
+
? { v: 1 as const, delimiter: READ_PROJECTION_DELIMITER, types: [...namespacedAggs].map(qaType).sort() }
|
|
826
|
+
: undefined;
|
|
747
827
|
// THE INVARIANT-GATE DECLARATION (#41): presence-only, derived from the SAME module
|
|
748
828
|
// the lump's executable bodies come from — sorted, omit-when-empty (hash-stable for
|
|
749
829
|
// invariant-free law; see the `UsdLayer.nomosInvariantGate` banner for the era rule).
|
|
750
830
|
const invariantAggregates = l.module.aggregates
|
|
751
831
|
.filter((a) => a.hasInvariant === true)
|
|
752
|
-
.map((a) => a.id)
|
|
832
|
+
.map((a) => qaType(a.id))
|
|
753
833
|
.sort();
|
|
754
834
|
const wsInvariants = [...(l.module.workspaceInvariants ?? [])]
|
|
755
835
|
.map((w) => ({ id: w.id, on: w.on }))
|
|
@@ -882,6 +962,17 @@ export function emitUsd(
|
|
|
882
962
|
.sort();
|
|
883
963
|
const nomosBirthGate =
|
|
884
964
|
birthDirectives.length > 0 ? { v: 1 as const, directives: birthDirectives } : undefined;
|
|
965
|
+
// THE BIRTH-RECORD GATE (metered birth, slice A1 — the framework's new era key). Present IFF
|
|
966
|
+
// this layer declares ≥1 `.births()` directive AND was compiled under this compiler: the law's
|
|
967
|
+
// EXPLICIT declaration that every birth it performs ALSO writes a structural NomosBirthRecord
|
|
968
|
+
// in the SAME intent (the `.births()` lane appends it automatically). The kernel's
|
|
969
|
+
// `verify_births_witness` reads THIS key (never a version-if): a carried birth whose re-derived
|
|
970
|
+
// events contain no `<prefix><child>` record event is a typed refusal (`birth-record-missing`).
|
|
971
|
+
// Omit-when-empty (the era rule): every pre-feature bundle lacks the key and births as today.
|
|
972
|
+
const nomosBirthRecordGate =
|
|
973
|
+
birthDirectives.length > 0
|
|
974
|
+
? { v: 1 as const, aggregate: "NomosBirthRecord", prefix: "nomos-birth-record:" }
|
|
975
|
+
: undefined;
|
|
885
976
|
// THE PEER-SOVEREIGN ROOT-OF-TRUST: the home/child law declared it requires a parent-attested-birth
|
|
886
977
|
// cert chain (the `requireParentAttestedBirth` sentinel). Emit the era key with the LAW-DECLARED owner +
|
|
887
978
|
// cert locators (so the kernel reads them, names neither); a bare marker (no locators) falls back to the
|
|
@@ -1011,9 +1102,18 @@ export function emitUsd(
|
|
|
1011
1102
|
// FIRST-CLASS VALUE OBJECTS: the module's collected descriptors (composeDomainModule
|
|
1012
1103
|
// dedupes + sorts them). Omit-when-empty (era rule — VO-free law is byte-identical).
|
|
1013
1104
|
const moduleValueObjects = (l.module as { valueObjects?: readonly import("./fields.js").ValueObjectDescriptor[] }).valueObjects;
|
|
1105
|
+
// THE VO↔FIELD ATTACHMENT: the usage pointers ride the SAME additive key (see
|
|
1106
|
+
// the UsdLayer type) — computed from the live Field metas, sorted, omit-when-empty.
|
|
1107
|
+
const voUsages = collectValueObjectUsages(
|
|
1108
|
+
l.module.aggregates as unknown as readonly { id: string; fields: Record<string, import("./fields.js").Field> }[],
|
|
1109
|
+
);
|
|
1014
1110
|
const nomosValueObjects =
|
|
1015
1111
|
moduleValueObjects !== undefined && moduleValueObjects.length > 0
|
|
1016
|
-
? {
|
|
1112
|
+
? {
|
|
1113
|
+
v: 1 as const,
|
|
1114
|
+
valueObjects: [...moduleValueObjects],
|
|
1115
|
+
...(voUsages.length > 0 ? { usages: voUsages } : {}),
|
|
1116
|
+
}
|
|
1017
1117
|
: undefined;
|
|
1018
1118
|
const flowEndorse = [...l.module.directives]
|
|
1019
1119
|
.flatMap((d) => harvestPrincipalFlows(d as never).map((f) => ({ directiveId: d.id, sink: f.sink, facet: f.facet, reason: f.reason })))
|
|
@@ -1024,9 +1124,9 @@ export function emitUsd(
|
|
|
1024
1124
|
);
|
|
1025
1125
|
return {
|
|
1026
1126
|
path: l.path,
|
|
1027
|
-
prims: encodeModuleToPrims(l.path,
|
|
1127
|
+
prims: encodeModuleToPrims(l.path, manifest),
|
|
1028
1128
|
nomosIdentityManifest: manifest,
|
|
1029
|
-
nomosReadProjection: readProjectionForModule(l.module, manifest),
|
|
1129
|
+
nomosReadProjection: readProjectionForModule(l.module, manifest, qaType),
|
|
1030
1130
|
...(manifest.queries !== undefined ? { queries: manifest.queries } : {}),
|
|
1031
1131
|
...(manifest.deriveds !== undefined ? { deriveds: manifest.deriveds } : {}),
|
|
1032
1132
|
...(manifest.combineds !== undefined ? { combineds: manifest.combineds } : {}),
|
|
@@ -1034,7 +1134,9 @@ export function emitUsd(
|
|
|
1034
1134
|
...(manifest.stableIds !== undefined ? { nomosStableIds: manifest.stableIds } : {}),
|
|
1035
1135
|
// THE STRUCTURAL INTERFACE (#72): the on-ledger OFFER — a client negotiates shape-
|
|
1036
1136
|
// compatibility against this, reactively, straight from the folded `policy:{hash}.bundle`.
|
|
1037
|
-
|
|
1137
|
+
// The client-facing structural offer stays BARE-labelled (labels are cosmetic there;
|
|
1138
|
+
// shape-compat negotiates by sid) — pass the pre-namespacing stable-ids.
|
|
1139
|
+
nomosInterface: domainInterface(l.module, bareManifest.stableIds),
|
|
1038
1140
|
...(Object.keys(readGateDirectives).length > 0
|
|
1039
1141
|
? {
|
|
1040
1142
|
nomosReadGate: {
|
|
@@ -1055,9 +1157,11 @@ export function emitUsd(
|
|
|
1055
1157
|
: {}),
|
|
1056
1158
|
...(nomosRelations !== undefined ? { nomosRelations } : {}),
|
|
1057
1159
|
...(nomosRelationNamespace !== undefined ? { nomosRelationNamespace } : {}),
|
|
1160
|
+
...(nomosReadProjectionNamespace !== undefined ? { nomosReadProjectionNamespace } : {}),
|
|
1058
1161
|
...(nomosRelationGate !== undefined ? { nomosRelationGate } : {}),
|
|
1059
1162
|
...(nomosAttestationGate !== undefined ? { nomosAttestationGate } : {}),
|
|
1060
1163
|
...(nomosBirthGate !== undefined ? { nomosBirthGate } : {}),
|
|
1164
|
+
...(nomosBirthRecordGate !== undefined ? { nomosBirthRecordGate } : {}),
|
|
1061
1165
|
...(nomosParentAttestedBirth !== undefined ? { nomosParentAttestedBirth } : {}),
|
|
1062
1166
|
...(nomosVerifiedFirstEnroll !== undefined ? { nomosVerifiedFirstEnroll } : {}),
|
|
1063
1167
|
...(nomosValueObjects !== undefined ? { nomosValueObjects } : {}),
|
|
@@ -1086,6 +1190,12 @@ export function emitUsd(
|
|
|
1086
1190
|
// package's per-domain layers: UNION every domain's relations on a shared bare-id type (so nothing
|
|
1087
1191
|
// is silently dropped) and REFUSE on a divergent shared rewrite (the scalpel). Non-shared /
|
|
1088
1192
|
// auto-qualified types are untouched → a package with no shared global type is byte-identical.
|
|
1193
|
+
// THE READ-PROJECTION DIVERGENCE GUARD (the scalpel, extended to the read layer): for keys that
|
|
1194
|
+
// remain SHARED across this package's domains (GLOBAL framework aggregates / same-named declared
|
|
1195
|
+
// reads), identical declarations dedupe at the kernel accumulator; a DIVERGENT shared key would
|
|
1196
|
+
// wedge the workspace at serve time — refuse it AT COMPILE instead, naming the key and each
|
|
1197
|
+
// domain's shape. Validation only (never transforms), so a conflict-free package is untouched.
|
|
1198
|
+
guardSharedReadProjectionKeys(out);
|
|
1089
1199
|
return { layers: composeSharedRelationTypes(out) };
|
|
1090
1200
|
}
|
|
1091
1201
|
|