@githolon/dsl 0.33.0 → 0.34.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/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/compile_engine.ts +13 -2
- package/src/compile_package_main.ts +16 -0
- 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 +42 -1
- 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
package/src/engine_entry.ts
CHANGED
|
@@ -54,8 +54,11 @@
|
|
|
54
54
|
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
55
55
|
import { expandCapabilityExports } from "./capability_exports.js";
|
|
56
56
|
import { RelationTuple, __setDispatchContext } from "./framework/relations.js";
|
|
57
|
+
import { namespacedAggregateIds, qualifyReadProjectionType, READ_PROJECTION_DELIMITER } from "./read_projection_namespace.js";
|
|
57
58
|
import { isRelationTypeDecl } from "./relation_schema.js";
|
|
58
59
|
import { synthesizePreconditionInvariants } from "./precondition.js";
|
|
60
|
+
import { synthesizeBirthQuotaInvariants } from "./birth_meter.js";
|
|
61
|
+
import { withBirthMeterExports } from "./birth_meter_ensure.js";
|
|
59
62
|
import { generateOpenApiFragment, mergeOpenApiFragments, type OpenApiDoc } from "./codegen_openapi.js";
|
|
60
63
|
import type { QueryDecl } from "./query.js";
|
|
61
64
|
import type { AnyCount } from "./count.js";
|
|
@@ -243,7 +246,10 @@ function mergeModules(mods: readonly DomainModuleExports[]): DomainModuleExports
|
|
|
243
246
|
// `capability()` bundles expand into discoverable entries here — the engine
|
|
244
247
|
// plane's ONE merge point, so the registry sees the task aggregate + quartet
|
|
245
248
|
// without per-piece re-exports (capability_exports.ts).
|
|
246
|
-
|
|
249
|
+
// METERED BIRTH: a `.births()`-declaring domain gains the birth-meter structural exports
|
|
250
|
+
// (the sanctioning `nomosBirthRecordEnsure` + the meter aggregates) at this same seam, so
|
|
251
|
+
// the registry + `describe()` see the exact surface the composed manifest carries.
|
|
252
|
+
return withBirthMeterExports(expandCapabilityExports(merged));
|
|
247
253
|
}
|
|
248
254
|
|
|
249
255
|
/**
|
|
@@ -270,6 +276,13 @@ interface DomainSlice {
|
|
|
270
276
|
* type's relations (governance/signer/read-visibility) are EXCLUDED so their bare objects stay bare.
|
|
271
277
|
*/
|
|
272
278
|
qualifiableRelations: Set<string>;
|
|
279
|
+
/**
|
|
280
|
+
* READ-PROJECTION NAMESPACING classification: the NON-GLOBAL aggregate ids this domain declares —
|
|
281
|
+
* the set whose runtime `__type` stamp qualifies to `<domain>__<type>` (matching the qualified
|
|
282
|
+
* keys the compiler wrote into the read projection / prims / stable-ids). GLOBAL aggregates (the
|
|
283
|
+
* explicit framework allowlist / `{global:true}`) are excluded so their bare identity survives.
|
|
284
|
+
*/
|
|
285
|
+
qualifiableAggregates: Set<string>;
|
|
273
286
|
}
|
|
274
287
|
|
|
275
288
|
/** Build one domain's slice from its merged module exports (the original scans). */
|
|
@@ -376,6 +389,13 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
|
|
|
376
389
|
for (const directive of dirs.values()) {
|
|
377
390
|
for (const w of synthesizePreconditionInvariants(directive)) wsInvariants.set(w.id, w);
|
|
378
391
|
}
|
|
392
|
+
// METERED BIRTH (slice A2 — mirror of composeDomainModule): synthesize THE BIRTH-QUOTA invariant's
|
|
393
|
+
// executable body for every `.births()` directive, so the SEALED ENGINE registers the same
|
|
394
|
+
// `reads`/`assert` the manifest's `nomosInvariantGate` entry names. Byte-identical for a domain
|
|
395
|
+
// with no birth directive (synthesizes none — the era rule).
|
|
396
|
+
for (const directive of dirs.values()) {
|
|
397
|
+
for (const w of synthesizeBirthQuotaInvariants(directive)) wsInvariants.set(w.id, w);
|
|
398
|
+
}
|
|
379
399
|
|
|
380
400
|
// ── RELATION-NAMESPACING classification — the LOCAL non-global `(type,relation)` set (by shape) ──
|
|
381
401
|
// A bare `writeTuple({object:"<type>:<id>", relation})` auto-qualifies iff `<type>#<relation>` is here.
|
|
@@ -388,7 +408,11 @@ function buildDomainSlice(mod: DomainModuleExports): DomainSlice {
|
|
|
388
408
|
for (const rel of v.relations.keys()) qualifiableRelations.add(`${v.type}#${rel}`);
|
|
389
409
|
}
|
|
390
410
|
|
|
391
|
-
|
|
411
|
+
// ── READ-PROJECTION NAMESPACING classification — the LOCAL non-global aggregate ids (one shared
|
|
412
|
+
// classification with the compiler: `read_projection_namespace.ts`). ──
|
|
413
|
+
const qualifiableAggregates = namespacedAggregateIds([...aggs.values()]);
|
|
414
|
+
|
|
415
|
+
return { registry, deriveds, combineds, invariants, aggInvariants, wsInvariants, qualifiableRelations, qualifiableAggregates };
|
|
392
416
|
}
|
|
393
417
|
|
|
394
418
|
/**
|
|
@@ -413,9 +437,14 @@ export function collectEngineRouting(
|
|
|
413
437
|
};
|
|
414
438
|
for (const [domainName, mods] of Object.entries(domains)) {
|
|
415
439
|
const slice = buildDomainSlice(mergeModules(mods));
|
|
416
|
-
|
|
417
|
-
for
|
|
418
|
-
|
|
440
|
+
// Type-keyed dispatches arrive with the ROW's type — QUALIFIED for a namespaced aggregate (the
|
|
441
|
+
// runtime `__type` stamp), bare for a global one / a pre-namespacing row. Route BOTH keys so
|
|
442
|
+
// either era resolves to the declaring domain (the slice lookup strips the qualifier back off).
|
|
443
|
+
const routeKeys = (type: string): string[] =>
|
|
444
|
+
slice.qualifiableAggregates.has(type) ? [type, qualifyReadProjectionType(domainName, type)] : [type];
|
|
445
|
+
for (const type of slice.deriveds.keys()) for (const k of routeKeys(type)) add(derivedOf, k, domainName);
|
|
446
|
+
for (const type of slice.combineds.keys()) for (const k of routeKeys(type)) add(combinedOf, k, domainName);
|
|
447
|
+
for (const type of slice.aggInvariants.keys()) for (const k of routeKeys(type)) add(aggregateInvariantOf, k, domainName);
|
|
419
448
|
for (const relationId of slice.invariants.keys()) add(relationOf, relationId, domainName);
|
|
420
449
|
for (const wsInvariantId of slice.wsInvariants.keys()) add(workspaceInvariantOf, wsInvariantId, domainName);
|
|
421
450
|
}
|
|
@@ -509,6 +538,24 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
509
538
|
return domainNames;
|
|
510
539
|
}
|
|
511
540
|
|
|
541
|
+
/**
|
|
542
|
+
* Resolve a TYPE-keyed registry entry for a dispatch that may carry a QUALIFIED type name
|
|
543
|
+
* (read-projection namespacing — the runtime `__type` stamp is `<domain>__<type>`): try the
|
|
544
|
+
* presented key, then the bare remainder after this domain's own qualifier. Registries stay
|
|
545
|
+
* keyed by the module's bare handle ids (the source of truth); this is the one un-qualify seam.
|
|
546
|
+
*/
|
|
547
|
+
function typeKeyed<T>(map: Map<string, T> | undefined, domainName: string, type: string): T | undefined {
|
|
548
|
+
if (map === undefined) return undefined;
|
|
549
|
+
const direct = map.get(type);
|
|
550
|
+
if (direct !== undefined) return direct;
|
|
551
|
+
// Strip the domain qualifier at the LAST delimiter — bare type names never contain `.`,
|
|
552
|
+
// and the qualifier's domain half is mint-tag-safe-ENCODED (`_`→`-`), so a raw-domainName
|
|
553
|
+
// prefix compare would miss (`co2_platform` qualifies as `co2-platform.`).
|
|
554
|
+
void domainName;
|
|
555
|
+
const i = type.lastIndexOf(READ_PROJECTION_DELIMITER);
|
|
556
|
+
return i > 0 ? map.get(type.slice(i + READ_PROJECTION_DELIMITER.length)) : undefined;
|
|
557
|
+
}
|
|
558
|
+
|
|
512
559
|
const REPORTS: Record<string, (actor: string) => EngineReport> = config.reports ?? {};
|
|
513
560
|
|
|
514
561
|
// ── the five planReport dispatch branches (wire shapes are the host oracle's contract) ──
|
|
@@ -555,7 +602,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
555
602
|
// Later domain wins (the Map.set fold order of the one flat registry).
|
|
556
603
|
let body: AggregateInvariantFn | undefined;
|
|
557
604
|
for (const domainName of domainsFor(routing.aggregateInvariantOf, aggregateType)) {
|
|
558
|
-
const found = sliceOf(domainName)?.aggInvariants
|
|
605
|
+
const found = typeKeyed(sliceOf(domainName)?.aggInvariants, domainName, aggregateType);
|
|
559
606
|
if (found !== undefined) body = found;
|
|
560
607
|
}
|
|
561
608
|
// VACUOUS HOLDS: a type with NO declared invariant trivially holds — `{accept:true}`,
|
|
@@ -680,7 +727,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
680
727
|
// Domain declaration order, then module export order — the eager fold order.
|
|
681
728
|
const fields: DerivedDecl[] = [];
|
|
682
729
|
for (const domainName of domainsFor(routing.derivedOf, ofType)) {
|
|
683
|
-
fields.push(...(sliceOf(domainName)?.deriveds
|
|
730
|
+
fields.push(...(typeKeyed(sliceOf(domainName)?.deriveds, domainName, ofType) ?? []));
|
|
684
731
|
}
|
|
685
732
|
const prior = (job.priorState ?? {}) as Record<string, unknown>;
|
|
686
733
|
const out: Record<string, unknown> = {};
|
|
@@ -705,7 +752,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
705
752
|
// Domain declaration order, then module export order — the eager fold order.
|
|
706
753
|
const fields: CombinedDecl[] = [];
|
|
707
754
|
for (const domainName of domainsFor(routing.combinedOf, ofType)) {
|
|
708
|
-
fields.push(...(sliceOf(domainName)?.combineds
|
|
755
|
+
fields.push(...(typeKeyed(sliceOf(domainName)?.combineds, domainName, ofType) ?? []));
|
|
709
756
|
}
|
|
710
757
|
const owner = (job.priorState ?? {}) as Record<string, unknown>;
|
|
711
758
|
// Preferred: related rows keyed by combined field id (iteration-order independent);
|
|
@@ -822,7 +869,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
|
|
|
822
869
|
// token qualifies explicitly. (`module.domain` === `intent.domain`.) Synchronous plan execution ⇒
|
|
823
870
|
// cleared in the `finally`.
|
|
824
871
|
const slice = sliceOf(domain)!;
|
|
825
|
-
__setDispatchContext({ domain, qualifiable: slice.qualifiableRelations });
|
|
872
|
+
__setDispatchContext({ domain, qualifiable: slice.qualifiableRelations, qualifiableAggregates: slice.qualifiableAggregates });
|
|
826
873
|
let wire: ReturnType<typeof executeDirectiveToIntent>;
|
|
827
874
|
try {
|
|
828
875
|
wire = executeDirectiveToIntent(entry.directive, entry.agg, intent.payload as never, ctx);
|
|
@@ -0,0 +1,125 @@
|
|
|
1
|
+
// NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
|
|
2
|
+
// remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon
|
|
3
|
+
// wasm32-wasip1 artifact {kernel · projection · embedded
|
|
4
|
+
// QuickJS engine}. If a file isn't this / hosting this / authoring for this / proving this — it's gone.
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* framework/birth_meter — THE BIRTH-ALLOWANCE GOVERNANCE SURFACE (metered birth, slice A2/A3).
|
|
8
|
+
*
|
|
9
|
+
* The meter's STRUCTURE (record + queries + quota invariant + auto-capture) is auto-ensured on every
|
|
10
|
+
* `.births()` law — see `../birth_meter.ts`. THIS module is the part a tenant COMPOSES IN when they
|
|
11
|
+
* want to GOVERN the allowance instead of living on the conservative default:
|
|
12
|
+
*
|
|
13
|
+
* - {@link setBirthPolicy} — set/amend the workspace's default allowance (admin-gated).
|
|
14
|
+
* - {@link grantBirthAllowance} — a per-subject override (admin-gated; latest grant wins).
|
|
15
|
+
* - {@link seedBirthPolicy} — THE GENESIS SEED (A3, ungated like `seedGenesis`): the step a
|
|
16
|
+
* parent's birth recipe threads (`buildGenesisChain`'s `descendantPool`) so the child is BORN
|
|
17
|
+
* BOUNDED — the pool chains to root by induction (root bounds platforms, platforms bound
|
|
18
|
+
* homes, homes bound estates). Control-plane protected exactly like `seedGenesis`: the open
|
|
19
|
+
* session lane structurally refuses control-plane intents, and post-genesis writes on a
|
|
20
|
+
* warranted workspace require signed authorship.
|
|
21
|
+
*
|
|
22
|
+
* V1 BOUNDARY (deliberate, stated in `architecture/metered_birth_and_attested_reads.md`): each node
|
|
23
|
+
* enforces ITS OWN count-quota; cross-level pool CONSERVATION arithmetic (a node may not grant its
|
|
24
|
+
* children more than it holds) is v2.
|
|
25
|
+
*/
|
|
26
|
+
import { z } from "zod";
|
|
27
|
+
import { instance } from "../aggregate.js";
|
|
28
|
+
import { directive } from "../directive.js";
|
|
29
|
+
import { set, type PlannedOp } from "../ops.js";
|
|
30
|
+
import {
|
|
31
|
+
BirthPolicy,
|
|
32
|
+
BirthGrant,
|
|
33
|
+
BIRTH_POLICY_ID,
|
|
34
|
+
birthGrantId,
|
|
35
|
+
} from "../birth_meter.js";
|
|
36
|
+
|
|
37
|
+
// Re-export the whole meter core so `@githolon/dsl` framework consumers get one import site.
|
|
38
|
+
export {
|
|
39
|
+
NomosBirthRecord,
|
|
40
|
+
BirthPolicy,
|
|
41
|
+
BirthGrant,
|
|
42
|
+
BIRTH_POLICY_ID,
|
|
43
|
+
NOMOS_BIRTH_RECORD_PREFIX,
|
|
44
|
+
birthRecordId,
|
|
45
|
+
birthGrantId,
|
|
46
|
+
birthRecordsByPrincipal,
|
|
47
|
+
birthPolicy,
|
|
48
|
+
birthGrantByPrincipal,
|
|
49
|
+
DEFAULT_MAX_BIRTHS_PER_PRINCIPAL,
|
|
50
|
+
birthQuotaVerdict,
|
|
51
|
+
birthQuotaInvariantId,
|
|
52
|
+
} from "../birth_meter.js";
|
|
53
|
+
|
|
54
|
+
/** The BirthPolicy singleton upsert ops (shared by the governed set and the genesis seed). */
|
|
55
|
+
function birthPolicyOps(p: { maxBirthsPerPrincipal: number; setBy: string; setAt: string }): PlannedOp[] {
|
|
56
|
+
const pol = instance(BirthPolicy, BIRTH_POLICY_ID);
|
|
57
|
+
return [
|
|
58
|
+
set(pol, "scope", "workspace"),
|
|
59
|
+
set(pol, "maxBirthsPerPrincipal", p.maxBirthsPerPrincipal),
|
|
60
|
+
set(pol, "setBy", p.setBy),
|
|
61
|
+
set(pol, "setAt", p.setAt),
|
|
62
|
+
];
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* setBirthPolicy — set (or amend) THE WORKSPACE'S BIRTH ALLOWANCE DEFAULT. An Ensure on the
|
|
67
|
+
* law-state singleton (deterministic id — the `setCloudPolicy` shape), gated on the workspace's
|
|
68
|
+
* admin relation (the existing relation-gate idiom).
|
|
69
|
+
*/
|
|
70
|
+
export const setBirthPolicy = directive("setBirthPolicy")
|
|
71
|
+
.ensures(BirthPolicy)
|
|
72
|
+
.payload(
|
|
73
|
+
z.object({
|
|
74
|
+
maxBirthsPerPrincipal: z.number().int().positive(),
|
|
75
|
+
setBy: z.string().min(1),
|
|
76
|
+
/** Caller-stamped (the timestamp doctrine). */
|
|
77
|
+
setAt: z.string().min(1),
|
|
78
|
+
}),
|
|
79
|
+
)
|
|
80
|
+
.plan((p) => birthPolicyOps(p))
|
|
81
|
+
.requires("admin");
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* seedBirthPolicy — THE GENESIS SEED (A3). Identical ops to {@link setBirthPolicy} but UNGATED,
|
|
85
|
+
* because it runs INSIDE a child's genesis recipe (before any admin relation exists) — exactly the
|
|
86
|
+
* `seedGenesis` idiom: a birth-door control-plane write the open session lane structurally refuses,
|
|
87
|
+
* and which post-genesis signed-authorship protects on every warranted workspace.
|
|
88
|
+
*/
|
|
89
|
+
export const seedBirthPolicy = directive("seedBirthPolicy")
|
|
90
|
+
.ensures(BirthPolicy)
|
|
91
|
+
.payload(
|
|
92
|
+
z.object({
|
|
93
|
+
maxBirthsPerPrincipal: z.number().int().positive(),
|
|
94
|
+
setBy: z.string().min(1),
|
|
95
|
+
setAt: z.string().min(1),
|
|
96
|
+
}),
|
|
97
|
+
)
|
|
98
|
+
.plan((p) => birthPolicyOps(p));
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* grantBirthAllowance — a PER-SUBJECT allowance override (the `grantQuota` shape): the LATEST grant
|
|
102
|
+
* (max `grantedAt`) for a subject is the allowance in force, overriding the BirthPolicy default.
|
|
103
|
+
* Admin-gated via the existing relation-gate idiom.
|
|
104
|
+
*/
|
|
105
|
+
export const grantBirthAllowance = directive("grantBirthAllowance")
|
|
106
|
+
.ensures(BirthGrant)
|
|
107
|
+
.payload(
|
|
108
|
+
z.object({
|
|
109
|
+
principal: z.string().min(1),
|
|
110
|
+
maxBirths: z.number().int().positive(),
|
|
111
|
+
grantedBy: z.string().min(1),
|
|
112
|
+
/** epoch-ms — the recency tiebreak (latest grant wins), caller-stamped. */
|
|
113
|
+
grantedAt: z.number().int().nonnegative(),
|
|
114
|
+
}),
|
|
115
|
+
)
|
|
116
|
+
.plan((p) => {
|
|
117
|
+
const g = instance(BirthGrant, birthGrantId(p.principal));
|
|
118
|
+
return [
|
|
119
|
+
set(g, "principal", p.principal),
|
|
120
|
+
set(g, "maxBirths", p.maxBirths),
|
|
121
|
+
set(g, "grantedBy", p.grantedBy),
|
|
122
|
+
set(g, "grantedAt", p.grantedAt),
|
|
123
|
+
];
|
|
124
|
+
})
|
|
125
|
+
.requires("admin");
|
|
@@ -165,7 +165,10 @@ export function impureCapabilityAggregate<
|
|
|
165
165
|
const Id extends string,
|
|
166
166
|
P extends Record<string, Field>,
|
|
167
167
|
>(id: Id, params: P): AggregateHandle<Id, typeof IMPURE_CAPABILITY_ENVELOPE & P> {
|
|
168
|
-
|
|
168
|
+
// GLOBAL (read-projection namespacing opt-out): the capability lane addresses the envelope
|
|
169
|
+
// by its BARE declared id (`nomosCapabilities.aggregateId` → the obligation rows the holon
|
|
170
|
+
// surfaces), so the envelope's type name must not be domain-qualified.
|
|
171
|
+
return aggregate(id, { ...IMPURE_CAPABILITY_ENVELOPE, ...params }, { global: true });
|
|
169
172
|
}
|
|
170
173
|
|
|
171
174
|
/**
|
|
@@ -235,7 +238,7 @@ const deadLetterSchema = z.object({
|
|
|
235
238
|
* handle; the envelope handle just types the envelope subset for `set()`).
|
|
236
239
|
*/
|
|
237
240
|
function envelopeInstance(aggregateId: string, requestId: string) {
|
|
238
|
-
const envelopeHandle = aggregate(aggregateId, IMPURE_CAPABILITY_ENVELOPE);
|
|
241
|
+
const envelopeHandle = aggregate(aggregateId, IMPURE_CAPABILITY_ENVELOPE, { global: true });
|
|
239
242
|
return instance(envelopeHandle, impureCapabilityInstanceId(requestId));
|
|
240
243
|
}
|
|
241
244
|
|
|
@@ -188,7 +188,10 @@ export function seedPlatformAdmin(opts: SeedPlatformAdminOptions): PlatformAdmin
|
|
|
188
188
|
relation: t.string().merge(Lww),
|
|
189
189
|
/** ISO-8601, caller-stamped (determinism — no clock in the plan). */
|
|
190
190
|
seededAt: t.string().merge(Lww),
|
|
191
|
-
|
|
191
|
+
// GLOBAL (read-projection namespacing opt-out): a dynamically-named framework aggregate —
|
|
192
|
+
// platform governance law deploys under differing domain keys (root vs each platform), and the
|
|
193
|
+
// founder singleton's bare identity must stay stable across them.
|
|
194
|
+
}, { global: true });
|
|
192
195
|
|
|
193
196
|
/**
|
|
194
197
|
* GENESIS SEED 1 — stamp the founder singleton at the fixed id. Authored by the platform's birth/genesis
|
|
@@ -48,6 +48,7 @@ import { set, setEntry, withMarker, type PlannedOp } from "../ops.js";
|
|
|
48
48
|
import { query } from "../query.js";
|
|
49
49
|
import { endorsementOf, type AttributionPrincipal, type EndorsedAuthority } from "../principal_evidence.js";
|
|
50
50
|
import { isRelationObjectToken, qualifyRelationType, type RelationObjectToken } from "../relation_schema.js";
|
|
51
|
+
import { qualifyReadProjectionType } from "../read_projection_namespace.js";
|
|
51
52
|
|
|
52
53
|
// ─────────────────────────────── the dispatch context (relation-namespacing) ───────────────────────────────
|
|
53
54
|
//
|
|
@@ -71,6 +72,12 @@ interface DispatchContext {
|
|
|
71
72
|
readonly domain: string;
|
|
72
73
|
/** `<type>#<relation>` for every LOCAL non-global authored relation of this domain — the qualify set. */
|
|
73
74
|
readonly qualifiable: ReadonlySet<string>;
|
|
75
|
+
/**
|
|
76
|
+
* READ-PROJECTION NAMESPACING: the NON-GLOBAL aggregate ids of the dispatching domain — the set
|
|
77
|
+
* whose event `__type` stamp qualifies to `<domain>__<type>` (matching the compiler's qualified
|
|
78
|
+
* read-projection / prim / stable-id keys). Absent (probe lanes) ⇒ no aggregate qualification.
|
|
79
|
+
*/
|
|
80
|
+
readonly qualifiableAggregates?: ReadonlySet<string>;
|
|
74
81
|
}
|
|
75
82
|
|
|
76
83
|
const EMPTY_QUALIFIABLE: ReadonlySet<string> = new Set<string>();
|
|
@@ -90,6 +97,18 @@ export function __setDispatchDomain(domain: string | undefined): void {
|
|
|
90
97
|
_ctx = domain === undefined ? undefined : { domain, qualifiable: EMPTY_QUALIFIABLE };
|
|
91
98
|
}
|
|
92
99
|
|
|
100
|
+
/**
|
|
101
|
+
* READ-PROJECTION NAMESPACING (the aggregate-type mirror of `resolveTupleObject`): qualify an event's
|
|
102
|
+
* aggregate TYPE by the dispatching domain IFF the domain declares it as a NON-GLOBAL aggregate — so the
|
|
103
|
+
* `__type` a plan's ops stamp equals the qualified schema key the compiler wrote (`usd.ts`). A global /
|
|
104
|
+
* foreign / unknown type, or a dispatch outside a plan (codegen probes), passes through BARE. Called by
|
|
105
|
+
* `wire_encode.ts::executeDirectiveToIntent` at the one event-emission point.
|
|
106
|
+
*/
|
|
107
|
+
export function __qualifyAggregateType(type: string): string {
|
|
108
|
+
if (_ctx === undefined || _ctx.qualifiableAggregates === undefined) return type;
|
|
109
|
+
return _ctx.qualifiableAggregates.has(type) ? qualifyReadProjectionType(_ctx.domain, type) : type;
|
|
110
|
+
}
|
|
111
|
+
|
|
93
112
|
/** Resolve a `writeTuple`/`removeTuple` object for a given relation (token → qualify; string → classify). */
|
|
94
113
|
function resolveTupleObject(object: string | RelationObjectToken, relation: string): string {
|
|
95
114
|
// A TOKEN is explicit sugar — always qualify its type by the dispatching domain.
|
|
@@ -482,6 +482,13 @@ export function buildGenesisChain(p: {
|
|
|
482
482
|
* chains arm the signature requirement.
|
|
483
483
|
*/
|
|
484
484
|
requiresWarrant?: boolean;
|
|
485
|
+
/**
|
|
486
|
+
* METERED BIRTH (slice A3 — the descendant budget). When present, the child's genesis includes a
|
|
487
|
+
* `seedBirthPolicy` step so it is BORN BOUNDED (its own default birth allowance per subject).
|
|
488
|
+
* OPT-IN: thread it only when the `lawHash` the order names carries `seedBirthPolicy` (a governance
|
|
489
|
+
* package compiled under the metered-birth DSL) — see the poolSteps note below.
|
|
490
|
+
*/
|
|
491
|
+
descendantPool?: number;
|
|
485
492
|
}): string {
|
|
486
493
|
const founder = "user:" + p.principal;
|
|
487
494
|
// THE ORDER-DECLARED LAW INSTALL (G3): the child installs the EXACT law the order names — bytes are NOT in
|
|
@@ -508,8 +515,29 @@ export function buildGenesisChain(p: {
|
|
|
508
515
|
authorizationHash: "",
|
|
509
516
|
parentHead: "",
|
|
510
517
|
});
|
|
518
|
+
// METERED BIRTH (slice A3 — THE POOL CHAINS TO ROOT): when the order carries a `descendantPool`, the
|
|
519
|
+
// child is BORN BOUNDED — a `seedBirthPolicy` genesis step writes the child's own BirthPolicy singleton
|
|
520
|
+
// (its default birth allowance) BEFORE the warrant lands, exactly like every other genesis seed (the
|
|
521
|
+
// still-warrant-free prefix runs unsigned). Root bounds platforms (this pool), platforms bound homes,
|
|
522
|
+
// homes bound estates — the tree meters by induction, with zero cross-workspace reads on any gate.
|
|
523
|
+
// OPT-IN (deliberate): the step folds through the CHILD'S OWN gate under the law hash the order names,
|
|
524
|
+
// so it may only be threaded when that law carries `seedBirthPolicy` (a law compiled under this DSL) —
|
|
525
|
+
// an unconditional default would refuse the genesis of every birth pinning an OLDER law hash. V1
|
|
526
|
+
// BOUNDARY: each node enforces its own count-quota; cross-level pool CONSERVATION arithmetic is v2.
|
|
527
|
+
const poolSteps =
|
|
528
|
+
p.descendantPool !== undefined
|
|
529
|
+
? [
|
|
530
|
+
{
|
|
531
|
+
domain: "workspaces",
|
|
532
|
+
directiveId: "seedBirthPolicy",
|
|
533
|
+
actor: founder,
|
|
534
|
+
payload: { maxBirthsPerPrincipal: p.descendantPool, setBy: p.principal, setAt: p.bornAt },
|
|
535
|
+
},
|
|
536
|
+
]
|
|
537
|
+
: [];
|
|
511
538
|
return JSON.stringify([
|
|
512
539
|
...installSteps,
|
|
540
|
+
...poolSteps,
|
|
513
541
|
// THE WARRANT-FLIP (THE GOVERNANCE DEFAULT): a governance birth is born WARRANTED by default — a REAL
|
|
514
542
|
// self-id (the workspace name) so signed control-plane authorship is enforced on every lane. The ONLY
|
|
515
543
|
// way to stay on the legacy host-stamped lane ("" self-id, byte-identical to a pre-VA chain) is an
|
|
@@ -926,6 +954,11 @@ export const birthChild = directive("birthChild")
|
|
|
926
954
|
* is born WARRANTED (signed authorship enforced on every lane). Absent/false ⇒ the legacy
|
|
927
955
|
* host-stamped lane (byte-identical to a pre-VA birth). Only meaningful with a genesis. */
|
|
928
956
|
requiresWarrant: z.boolean().optional(),
|
|
957
|
+
/** METERED BIRTH (slice A3): the child's own descendant budget — when present, the genesis
|
|
958
|
+
* recipe seeds the child's BirthPolicy (`seedBirthPolicy`) so it is BORN BOUNDED. OPT-IN:
|
|
959
|
+
* only pass it when the `lawHash` the order names carries `seedBirthPolicy` (a governance
|
|
960
|
+
* package compiled under the metered-birth DSL). Only meaningful with a genesis. */
|
|
961
|
+
descendantPool: z.number().int().positive().optional(),
|
|
929
962
|
})
|
|
930
963
|
// A genesis birth (a platform) must carry a principal AND a bornAt — it owns a subtree, a lineage
|
|
931
964
|
// anchor, and a law-authored genesis chain whose warrant is stamped at bornAt.
|
|
@@ -946,7 +979,7 @@ export const birthChild = directive("birthChild")
|
|
|
946
979
|
// EXPLICITLY opts out (`requiresWarrant === false`). Thread the flag through ONLY when the request set
|
|
947
980
|
// it — undefined falls to buildGenesisChain's warranted default; an explicit `false` rides the compat
|
|
948
981
|
// hatch (an era-safe legacy re-build).
|
|
949
|
-
const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}), ...(p.requiresWarrant !== undefined ? { requiresWarrant: p.requiresWarrant } : {}) });
|
|
982
|
+
const genesisChain = buildGenesisChain({ name: p.name, principal: p.principal, parent: p.parent, tier: p.tier, foundingAdminHash: p.foundingAdminHash, bornAt: p.bornAt as string, requestedAt: p.requestedAt, ...(p.frameworkHash ? { frameworkHash: p.frameworkHash } : {}), ...(p.lawHash ? { lawHash: p.lawHash } : {}), ...(p.requiresWarrant !== undefined ? { requiresWarrant: p.requiresWarrant } : {}), ...(p.descendantPool !== undefined ? { descendantPool: p.descendantPool } : {}) });
|
|
950
983
|
// THE GENESIS RECIPE IS ALSO A LINEAGE/AUDIT FACT on the parent's chain (the parent records what it
|
|
951
984
|
// ORDERED — law naming law, legitimate). The kernel does NOT read this field to detect the birth.
|
|
952
985
|
rec.set("genesisChain", genesisChain);
|
|
@@ -1645,3 +1678,11 @@ export const workspaceQuotaBirth = workspaceInvariant("WorkspaceQuotaBirth")
|
|
|
1645
1678
|
.assert((_snapshots, ctx) =>
|
|
1646
1679
|
quotaVerdict(ctx?.reads ?? [], String((ctx?.intent ?? {}).principal ?? "")),
|
|
1647
1680
|
);
|
|
1681
|
+
|
|
1682
|
+
// ── METERED BIRTH (slice A — the allowance governance in the workspaces domain) ─────────────────────
|
|
1683
|
+
// Re-exported here so any governance package composed from THIS module carries the birth-allowance
|
|
1684
|
+
// surface: `setBirthPolicy`/`grantBirthAllowance` (admin-gated) and `seedBirthPolicy` — the A3 genesis
|
|
1685
|
+
// seed `buildGenesisChain`'s `descendantPool` step dispatches as `workspaces/seedBirthPolicy` (it must
|
|
1686
|
+
// live in the SAME domain key the genesis recipe names). The meter's structural half (record + quota)
|
|
1687
|
+
// is auto-ensured by `.births()` — see `../birth_meter.ts`.
|
|
1688
|
+
export { setBirthPolicy, seedBirthPolicy, grantBirthAllowance, BirthPolicy, BirthGrant, NomosBirthRecord, birthRecordsByPrincipal, birthPolicy, birthGrantByPrincipal } from "./birth_meter.js";
|
package/src/index.ts
CHANGED
|
@@ -58,6 +58,7 @@ export {
|
|
|
58
58
|
type GeoPointValue,
|
|
59
59
|
type GeoLineStringValue,
|
|
60
60
|
type GeoPolygonValue,
|
|
61
|
+
type ValueObjectUsage,
|
|
61
62
|
} from "./value_object.js";
|
|
62
63
|
export {
|
|
63
64
|
type PrincipalEvidence,
|
|
@@ -331,6 +332,28 @@ export {
|
|
|
331
332
|
birthAttestation,
|
|
332
333
|
parentAttestedBirth,
|
|
333
334
|
} from "./framework/home.js";
|
|
335
|
+
// METERED BIRTH (slice A — architecture/metered_birth_and_attested_reads.md): the structural birth
|
|
336
|
+
// record + the parent-local birth quota + the allowance governance surface. The record/quota STRUCTURE
|
|
337
|
+
// is auto-ensured on every `.births()` law (zero tenant code); these exports are the GOVERNANCE half a
|
|
338
|
+
// tenant composes in to set/override allowances, plus the constants/predicates tests and tools read.
|
|
339
|
+
export {
|
|
340
|
+
NomosBirthRecord,
|
|
341
|
+
BirthPolicy,
|
|
342
|
+
BirthGrant,
|
|
343
|
+
BIRTH_POLICY_ID,
|
|
344
|
+
NOMOS_BIRTH_RECORD_PREFIX,
|
|
345
|
+
birthRecordId,
|
|
346
|
+
birthGrantId,
|
|
347
|
+
birthRecordsByPrincipal,
|
|
348
|
+
birthPolicy,
|
|
349
|
+
birthGrantByPrincipal,
|
|
350
|
+
DEFAULT_MAX_BIRTHS_PER_PRINCIPAL,
|
|
351
|
+
birthQuotaVerdict,
|
|
352
|
+
birthQuotaInvariantId,
|
|
353
|
+
setBirthPolicy,
|
|
354
|
+
seedBirthPolicy,
|
|
355
|
+
grantBirthAllowance,
|
|
356
|
+
} from "./framework/birth_meter.js";
|
|
334
357
|
// THE PLATFORM FOUNDING-ADMIN BOOTSTRAP (the platform analog of seedHomeCreator) — a tenant-importable
|
|
335
358
|
// factory that seats a tenant-named governance relation for a write-once, author-bound founder at genesis.
|
|
336
359
|
export {
|
|
@@ -457,6 +480,17 @@ export {
|
|
|
457
480
|
type AllowedSubject,
|
|
458
481
|
type RelationCaps,
|
|
459
482
|
} from "./relation_schema.js";
|
|
483
|
+
|
|
484
|
+
// READ-PROJECTION (aggregate-type) NAMESPACING — the read-layer mirror of the relation namespacing:
|
|
485
|
+
// the qualifier, the deliberate framework GLOBAL allowlist, and the classification the compiler +
|
|
486
|
+
// sealed engine share (`aggregate(id, fields, { global: true })` is the dynamic opt-out).
|
|
487
|
+
export {
|
|
488
|
+
READ_PROJECTION_DELIMITER,
|
|
489
|
+
GLOBAL_READ_PROJECTION_AGGREGATE_IDS,
|
|
490
|
+
qualifyReadProjectionType,
|
|
491
|
+
isNamespacedAggregate,
|
|
492
|
+
namespacedAggregateIds,
|
|
493
|
+
} from "./read_projection_namespace.js";
|
|
460
494
|
// SECURE-BY-DEFAULT SUGAR over the relation schema — plain-English combinators, audiences, presets.
|
|
461
495
|
// Pure sugar: every export here BUILDS the existing Rewrite/RelationTypeDecl AST (no new wire shape).
|
|
462
496
|
export {
|
package/src/read.ts
CHANGED
|
@@ -74,6 +74,28 @@ export function read<T = unknown>(query: QueryDecl, args: Record<string, string>
|
|
|
74
74
|
return result;
|
|
75
75
|
}
|
|
76
76
|
|
|
77
|
+
/**
|
|
78
|
+
* THE BIRTH-METER CAPTURE (metered birth, slice A2 — library-internal). Identical semantics to
|
|
79
|
+
* {@link read} — serve the declared query live off `nomos.read` + record `{queryId, args, result}`
|
|
80
|
+
* into the SAME per-dispatch footprint — but with a DISTINCT refusal string, deliberately NOT the
|
|
81
|
+
* {@link CAPTURED_READ_LUMP_MARKER}: the marker greps for TENANT-authored `read()` calls that lack a
|
|
82
|
+
* declaration, whereas this door is only ever invoked by the `.births()` lane (which auto-DECLARES
|
|
83
|
+
* its three queries on the directive), so a lump that merely bundles it must not trip the read-free
|
|
84
|
+
* compile gate. A `.births()` dispatch on a host with no `nomos.read` still halts NAMED (fail-closed).
|
|
85
|
+
*/
|
|
86
|
+
export function __captureBirthMeterRead(query: QueryDecl, args: Record<string, string>): void {
|
|
87
|
+
const h = host() as { read?: unknown } | undefined;
|
|
88
|
+
if (typeof h?.read !== "function") {
|
|
89
|
+
throw new Error(
|
|
90
|
+
`birth-meter read('${query.id}'): this host provides no nomos.read port — a .births() ` +
|
|
91
|
+
`dispatch captures its quota reads through the engine/client runtime (tests: stub ` +
|
|
92
|
+
`globalThis.nomos.read).`,
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
const result = host().read(query.id, args);
|
|
96
|
+
footprint.push({ queryId: query.id, args, result });
|
|
97
|
+
}
|
|
98
|
+
|
|
77
99
|
/**
|
|
78
100
|
* The compile gate's lump sentinel — the EXACT substring of the refusal `read()` throws above.
|
|
79
101
|
* It exists in a bundled engine lump iff `read` itself survived tree-shaking, i.e. iff some
|
|
@@ -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
|
+
}
|