@githolon/dsl 0.100.5 → 0.101.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/dart/CHANGELOG.md +12 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +220 -71
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +1 -1
- package/dart/test/adv_realm_interference_test.dart +1 -1
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/cluster_attested_test.dart +1 -1
- package/dart/test/cluster_births_test.dart +1 -1
- package/dart/test/cluster_device_keyed_acceptance_test.dart +1 -1
- package/dart/test/cluster_local_autosync_test.dart +1 -1
- package/dart/test/cluster_scale_test.dart +1 -1
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/dlq_session_surface_test.dart +1 -1
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +132 -87
- package/dart/test/local_sync_inert_test.dart +10 -5
- package/dart/test/parent_signer_recovery_test.dart +1 -1
- package/dart/test/sealing_client_test.dart +1 -1
- package/dart/test/session_bound_test.dart +1 -1
- package/dart/test/test_harness_test.dart +2 -2
- package/dart/test/vo_read_typing_test.dart +1 -1
- package/dart/test/watch_after_offer_stall_test.dart +1 -1
- package/dart/test/webview_bridge_test.dart +274 -0
- package/dart_types/CHANGELOG.md +6 -0
- package/dart_types/lib/src/errors.dart +136 -30
- package/dart_types/lib/src/surfaced.dart +48 -2
- package/dart_types/lib/src/wire.dart +3 -1
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/surfaced_test.dart +19 -0
- package/package.json +1 -1
- package/src/activity.ts +43 -4
- package/src/aggregate.ts +132 -22
- package/src/attested_core.ts +1 -1
- package/src/authoring.ts +119 -6
- package/src/birth_meter.ts +4 -4
- package/src/birth_meter_ensure.ts +165 -29
- package/src/births.ts +53 -22
- package/src/build_package.ts +96 -20
- package/src/business_birth.ts +79 -16
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +930 -87
- package/src/codegen_openapi.ts +28 -5
- package/src/codegen_proof.ts +107 -11
- package/src/codegen_ts.ts +961 -135
- package/src/compile_package_main.ts +172 -53
- package/src/conflicts.ts +252 -0
- package/src/describe_model.ts +448 -0
- package/src/directive.ts +5 -38
- package/src/drivers.ts +12 -4
- package/src/engine_entry.ts +4 -0
- package/src/evolve_preview.ts +72 -11
- package/src/fields.ts +201 -55
- package/src/framework/application_home.ts +103 -0
- package/src/framework/application_home_domain.ts +2 -0
- package/src/framework/attested_receipts.ts +2 -2
- package/src/framework/auth_provider_law.ts +51 -24
- package/src/framework/business_corrections.ts +170 -0
- package/src/framework/disclosure.ts +1 -1
- package/src/framework/domain_lifecycle.ts +250 -16
- package/src/framework/encryption_identity.ts +1 -1
- package/src/framework/encryption_scope.ts +21 -13
- package/src/framework/era_certification.ts +1 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +1 -1
- package/src/framework/home.ts +22 -9
- package/src/framework/identity.ts +5 -5
- package/src/framework/impure_capability.ts +2 -2
- package/src/framework/owned_custody.ts +561 -0
- package/src/framework/platform_admin.ts +1 -1
- package/src/framework/relations.ts +9 -1
- package/src/framework/repair.ts +1 -1
- package/src/framework/signing_identity.ts +2 -2
- package/src/framework/source_roster.ts +1 -1
- package/src/framework/workspaces.ts +17 -10
- package/src/generated_client_names.ts +39 -0
- package/src/index.ts +20 -15
- package/src/kernel_capabilities.ts +25 -1
- package/src/law_check.ts +4 -1
- package/src/manifest.ts +356 -63
- package/src/read_projection_manifest.ts +8 -0
- package/src/read_projection_namespace.ts +4 -0
- package/src/relation.ts +1 -1
- package/src/shape_compat.ts +61 -5
- package/src/stable_ids.ts +38 -6
- package/src/stable_ids_types.ts +30 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd.ts +62 -11
- package/src/usd_layers.ts +54 -3
- package/src/value_object.ts +23 -19
- package/src/wire.ts +1 -0
- package/src/wire_encode.ts +48 -4
- package/src/workspace_routing.ts +57 -0
- package/src/workspace_sharding.ts +9 -9
- package/src/certified_read.ts +0 -313
package/src/manifest.ts
CHANGED
|
@@ -28,9 +28,18 @@
|
|
|
28
28
|
import { createHash } from "node:crypto";
|
|
29
29
|
import { z } from "zod";
|
|
30
30
|
import { encodeKernelSchema, encodeRefMode } from "./wire_encode.js";
|
|
31
|
+
import { encodeDriver } from "./drivers.js";
|
|
31
32
|
import type { WireSchema, WireRefMode } from "./wire.js";
|
|
32
33
|
import type { ReferentialMarker } from "./directive.js";
|
|
33
34
|
import type { DomainModule } from "./codegen_dart.js";
|
|
35
|
+
import type { Field } from "./fields.js";
|
|
36
|
+
import {
|
|
37
|
+
BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
|
|
38
|
+
OWNED_CUSTODY_PAYLOAD_FIELD,
|
|
39
|
+
OWNED_KEY_PAYLOAD_FIELD,
|
|
40
|
+
OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
|
|
41
|
+
OWNED_READ_RELATION,
|
|
42
|
+
} from "./aggregate.js";
|
|
34
43
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
35
44
|
import type { QueryDecl } from "./query.js";
|
|
36
45
|
import { finishCount, type AnyCount } from "./count.js";
|
|
@@ -43,7 +52,6 @@ import type { CanonicalPred } from "./predicate.js";
|
|
|
43
52
|
import { assertFlowGate } from "./flow_gate.js";
|
|
44
53
|
import type { DerivedDecl } from "./derived.js";
|
|
45
54
|
import type { CombinedDecl } from "./combined.js";
|
|
46
|
-
import type { CertifiedReadDecl } from "./certified_read.js";
|
|
47
55
|
import type { RelationDecl } from "./relation.js";
|
|
48
56
|
import { type CanonicalWorkspaceType } from "./workspace_type.js";
|
|
49
57
|
import {
|
|
@@ -56,6 +64,7 @@ import {
|
|
|
56
64
|
mintFieldSid,
|
|
57
65
|
type StableAggregateId,
|
|
58
66
|
type StableFieldId,
|
|
67
|
+
type StableFieldEvolution,
|
|
59
68
|
type StableIds,
|
|
60
69
|
} from "./stable_ids.js";
|
|
61
70
|
|
|
@@ -107,6 +116,12 @@ export interface CanonicalAggregate {
|
|
|
107
116
|
* so adding/changing it MOVES the hash. The scope is the aggregate's `requiresRead`.
|
|
108
117
|
*/
|
|
109
118
|
readonly encryptedFields?: string[];
|
|
119
|
+
/** Secure-by-default owned lowering. Presence means `requiresRead` is a PER-RECORD relation (not a
|
|
120
|
+
* workspace-wide capability), `ownerField` is framework-stamped, and the encrypted slots use the
|
|
121
|
+
* stateful-client owned-key lane. */
|
|
122
|
+
readonly owned?: { readonly ownerField: string; readonly relation: string };
|
|
123
|
+
/** Stored framework fields intentionally hidden from generated business models. */
|
|
124
|
+
readonly internalFields?: readonly string[];
|
|
110
125
|
/**
|
|
111
126
|
* ATTRIBUTED READS (architecture/field_provenance.md) — the STORED field names whose
|
|
112
127
|
* folded values keep author provenance (`.attributed()`), SORTED. PRESENT only when
|
|
@@ -152,17 +167,6 @@ export interface CanonicalDirective {
|
|
|
152
167
|
* same omit-when-empty hash-invariance reason as {@link reads}.
|
|
153
168
|
*/
|
|
154
169
|
readonly emits?: Record<string, { max?: number }>;
|
|
155
|
-
/**
|
|
156
|
-
* The directive's DECLARED CertifiedReads (survival Constraint 5; `certified_read.md`): the
|
|
157
|
-
* profiled write-path reads its `decide()` consults, as `query_id → { sql, multiRow,
|
|
158
|
-
* uniqueTieBreakers }`. SORTED BY `query_id` (the SET of declared reads is the contract).
|
|
159
|
-
* Part of the domain identity: the gate DERIVES the required-read set from these `query_id`s
|
|
160
|
-
* (the vacuous-pass close), and the SQL RECIPE + shape are captured so a recipe change moves
|
|
161
|
-
* the hash (a different read = a different contract). OMITTED ENTIRELY when the directive
|
|
162
|
-
* declares no write-path read — so a read-free directive is byte-identical in the manifest to
|
|
163
|
-
* before this key existed (pinned golden hashes stay UNCHANGED).
|
|
164
|
-
*/
|
|
165
|
-
readonly certifiedReads?: CanonicalCertifiedRead[];
|
|
166
170
|
/**
|
|
167
171
|
* The directive's DECLARED CAPTURED-READ queries (#58 — the captured-read lane):
|
|
168
172
|
* the O(1) DSL queries its `plan` may `read()` on the write path, each carried as
|
|
@@ -185,6 +189,16 @@ export interface CanonicalDirective {
|
|
|
185
189
|
* UNCHANGED), exactly how {@link reads}/{@link relations} stay hash-neutral.
|
|
186
190
|
*/
|
|
187
191
|
readonly requiresHostCapability?: string[];
|
|
192
|
+
/**
|
|
193
|
+
* A law-declared business conflict resolver. The generated client carries an immutable
|
|
194
|
+
* framework snapshot beside this directive's payload, and every admission/replay door
|
|
195
|
+
* compare-and-sets it against the exact pending observation set. Omitted for ordinary
|
|
196
|
+
* directives, so ordinary business actions carry no conflict vocabulary.
|
|
197
|
+
*/
|
|
198
|
+
readonly conflictResolution?: {
|
|
199
|
+
readonly aggregate: string;
|
|
200
|
+
readonly field: string;
|
|
201
|
+
};
|
|
188
202
|
/**
|
|
189
203
|
* The directive's DECLARED cross-workspace relations (`cross_workspace.md` §2): the relations
|
|
190
204
|
* a cross-workspace PR proposing this directive is adjudicated against. Each carries the
|
|
@@ -233,15 +247,6 @@ export interface CanonicalCapturedRead {
|
|
|
233
247
|
readonly returns: string;
|
|
234
248
|
}
|
|
235
249
|
|
|
236
|
-
/** One captured DECLARED CertifiedRead — the profiled write-path read identity in the manifest.
|
|
237
|
-
* `queryId` is the gate's required-read key; `sql`/`multiRow`/`uniqueTieBreakers` are the recipe
|
|
238
|
-
* + shape the admission re-run + the profile pin (a recipe change moves the domain hash). */
|
|
239
|
-
export interface CanonicalCertifiedRead {
|
|
240
|
-
readonly queryId: string;
|
|
241
|
-
readonly sql: string;
|
|
242
|
-
readonly multiRow: boolean;
|
|
243
|
-
readonly uniqueTieBreakers: string[];
|
|
244
|
-
}
|
|
245
250
|
|
|
246
251
|
/**
|
|
247
252
|
* One captured query declaration — the read-side closure's named, INDEXED read.
|
|
@@ -705,16 +710,6 @@ function canonicalEmits(
|
|
|
705
710
|
return { emits };
|
|
706
711
|
}
|
|
707
712
|
|
|
708
|
-
/**
|
|
709
|
-
* Canonicalize a directive's declared CertifiedReads into a manifest fragment (survival
|
|
710
|
-
* Constraint 5). OMIT-WHEN-EMPTY: returns `{}` (no `certifiedReads` key) when the directive
|
|
711
|
-
* declares none — the omission keeps read-free hashes unchanged. When non-empty, returns a
|
|
712
|
-
* `certifiedReads` array SORTED BY `queryId` (the SET of declared reads is the contract), each
|
|
713
|
-
* carrying its `queryId` + the profiled `sql`/`multiRow`/`uniqueTieBreakers` recipe (so a recipe
|
|
714
|
-
* change moves the domain hash — a different read is a different contract). The local binding
|
|
715
|
-
* NAME (the `reads:{}` key) is incidental to identity (it is `decide`'s ergonomics), so it is
|
|
716
|
-
* NOT captured — only the `query_id` + recipe.
|
|
717
|
-
*/
|
|
718
713
|
/**
|
|
719
714
|
* Canonicalize a directive's DECLARED captured-read queries (#58) into a manifest
|
|
720
715
|
* fragment. OMIT-WHEN-EMPTY: returns `{}` (no `capturedReads` key) when the directive
|
|
@@ -732,22 +727,6 @@ function canonicalCapturedReads(
|
|
|
732
727
|
return { capturedReads };
|
|
733
728
|
}
|
|
734
729
|
|
|
735
|
-
function canonicalCertifiedReads(
|
|
736
|
-
declared: Record<string, CertifiedReadDecl>,
|
|
737
|
-
): { certifiedReads?: CanonicalCertifiedRead[] } {
|
|
738
|
-
const decls = Object.values(declared);
|
|
739
|
-
if (decls.length === 0) return {};
|
|
740
|
-
const certifiedReads: CanonicalCertifiedRead[] = decls
|
|
741
|
-
.map((d) => ({
|
|
742
|
-
queryId: d.queryId,
|
|
743
|
-
sql: d.sql,
|
|
744
|
-
multiRow: d.multiRow,
|
|
745
|
-
uniqueTieBreakers: [...d.uniqueTieBreakers],
|
|
746
|
-
}))
|
|
747
|
-
.sort((a, b) => (a.queryId < b.queryId ? -1 : a.queryId > b.queryId ? 1 : 0));
|
|
748
|
-
return { certifiedReads };
|
|
749
|
-
}
|
|
750
|
-
|
|
751
730
|
/**
|
|
752
731
|
* Canonicalize a directive's declared cross-workspace relations into a manifest fragment
|
|
753
732
|
* (`cross_workspace.md` §2). OMIT-WHEN-EMPTY: returns `{}` (no `relations` key) when the
|
|
@@ -1004,19 +983,31 @@ function stableIdsForModule(
|
|
|
1004
983
|
for (const agg of aggregates) {
|
|
1005
984
|
const prior = continuity?.aggregates[agg.id];
|
|
1006
985
|
const aggSid = prior?.sid ?? mintAggregateSid(domainSid, agg.id);
|
|
1007
|
-
const handleFields = (handlesById.get(agg.id)?.fields ?? {}) as Record<
|
|
1008
|
-
string,
|
|
1009
|
-
{ kind?: string; isFromBirth?: true } | undefined
|
|
1010
|
-
>;
|
|
986
|
+
const handleFields = (handlesById.get(agg.id)?.fields ?? {}) as Record<string, Field | undefined>;
|
|
1011
987
|
const fields: Record<string, StableFieldId> = {};
|
|
1012
988
|
for (const fieldName of Object.keys(agg.schema).sort()) {
|
|
1013
989
|
const pf = prior?.fields[fieldName];
|
|
1014
990
|
const kind = handleFields[fieldName]?.kind;
|
|
991
|
+
const handle = handleFields[fieldName];
|
|
992
|
+
const details = handle === undefined ? undefined : fieldValueContractDetails(handle);
|
|
993
|
+
const driver = handle === undefined ? undefined : encodeDriver(handle.driver);
|
|
994
|
+
const schema =
|
|
995
|
+
handle === undefined || kind === undefined || details === undefined || driver === undefined
|
|
996
|
+
? undefined
|
|
997
|
+
: fieldSchemaIdentity(aggSid, pf?.sid ?? mintFieldSid(aggSid, fieldName), kind, details.contract, driver);
|
|
998
|
+
const evolutions =
|
|
999
|
+
handle === undefined || schema === undefined || driver === undefined || details === undefined
|
|
1000
|
+
? []
|
|
1001
|
+
: fieldEvolutionRules(handle, pf, aggSid, pf?.sid ?? mintFieldSid(aggSid, fieldName), details, driver, schema);
|
|
1015
1002
|
fields[fieldName] = {
|
|
1016
1003
|
sid: pf?.sid ?? mintFieldSid(aggSid, fieldName),
|
|
1017
1004
|
...(pf?.was !== undefined && pf.was.length > 0 ? { was: pf.was } : {}),
|
|
1018
1005
|
// The CURRENT kind, always from the live module (continuity never supplies it).
|
|
1019
1006
|
...(kind !== undefined ? { kind } : {}),
|
|
1007
|
+
...(details !== undefined ? { contract: details.contract, shape: details.shape } : {}),
|
|
1008
|
+
...(schema !== undefined ? { schema } : {}),
|
|
1009
|
+
...(driver !== undefined ? { driver } : {}),
|
|
1010
|
+
...(evolutions.length > 0 ? { evolutions } : {}),
|
|
1020
1011
|
// Birth-presence law rides identity: the evolve guard reads it (fromBirth may not be
|
|
1021
1012
|
// NEWLY declared on a sid that existed without it).
|
|
1022
1013
|
...(handleFields[fieldName]?.isFromBirth === true ? { fromBirth: true as const } : {}),
|
|
@@ -1031,25 +1022,313 @@ function stableIdsForModule(
|
|
|
1031
1022
|
return { v: 1, domain: domainSid, aggregates: out };
|
|
1032
1023
|
}
|
|
1033
1024
|
|
|
1025
|
+
/**
|
|
1026
|
+
* Identity of the value a generated TS/Dart conflict resolver is allowed to call `T`.
|
|
1027
|
+
* This is deliberately narrower than the domain hash (unrelated law may roll out without
|
|
1028
|
+
* invalidating a conflict) and richer than `Field.kind` (enum arms, value-object members,
|
|
1029
|
+
* nullability, references and encrypted/plain decoding all matter to the public type).
|
|
1030
|
+
*/
|
|
1031
|
+
function fieldValueContractDetails(field: Field): { contract: string; shape: unknown } {
|
|
1032
|
+
const valueObjectContract = (meta: NonNullable<Field["valueObject"]>) => ({
|
|
1033
|
+
descriptor: meta.descriptor,
|
|
1034
|
+
dependencies: meta.dependencies,
|
|
1035
|
+
...(meta.geometry !== undefined ? { geometry: meta.geometry } : {}),
|
|
1036
|
+
});
|
|
1037
|
+
const contractShape = normalizeFieldContract({
|
|
1038
|
+
v: "nomos-field-value-contract-v1",
|
|
1039
|
+
kind: field.kind,
|
|
1040
|
+
jsonSchema: z.toJSONSchema(field.zod as z.ZodTypeAny),
|
|
1041
|
+
...(field.mapValueKind !== undefined ? { mapValueKind: field.mapValueKind } : {}),
|
|
1042
|
+
...(field.jsonShape !== undefined ? { jsonShape: field.jsonShape } : {}),
|
|
1043
|
+
...(field.valueObject !== undefined
|
|
1044
|
+
? { valueObject: valueObjectContract(field.valueObject) }
|
|
1045
|
+
: {}),
|
|
1046
|
+
...(field.mapValueVo !== undefined
|
|
1047
|
+
? { mapValueVo: valueObjectContract(field.mapValueVo) }
|
|
1048
|
+
: {}),
|
|
1049
|
+
...(field.refAggregateId !== undefined ? { refAggregateId: field.refAggregateId } : {}),
|
|
1050
|
+
...(field.foreignRef !== undefined ? { foreignRef: field.foreignRef } : {}),
|
|
1051
|
+
...(field.nullSemantic !== undefined ? { nullSemantic: field.nullSemantic } : {}),
|
|
1052
|
+
...(field.isClearable === true ? { clearable: true } : {}),
|
|
1053
|
+
...(field.isEncrypted === true ? { encrypted: true } : {}),
|
|
1054
|
+
});
|
|
1055
|
+
const contract = createHash("sha256")
|
|
1056
|
+
.update(
|
|
1057
|
+
`nomos-field-value-contract:v1:${canonicalJson(contractShape)}`,
|
|
1058
|
+
"utf8",
|
|
1059
|
+
)
|
|
1060
|
+
.digest("hex");
|
|
1061
|
+
return { contract, shape: contractShape };
|
|
1062
|
+
}
|
|
1063
|
+
|
|
1064
|
+
function fieldSchemaIdentity(
|
|
1065
|
+
aggregateSid: string,
|
|
1066
|
+
fieldSid: string,
|
|
1067
|
+
kind: string,
|
|
1068
|
+
valueContract: string,
|
|
1069
|
+
driver: unknown,
|
|
1070
|
+
): string {
|
|
1071
|
+
return createHash("sha256")
|
|
1072
|
+
.update(
|
|
1073
|
+
canonicalJson({
|
|
1074
|
+
v: "nomos-field-schema-v2",
|
|
1075
|
+
aggregate: aggregateSid,
|
|
1076
|
+
field: fieldSid,
|
|
1077
|
+
kind,
|
|
1078
|
+
valueContract,
|
|
1079
|
+
driver,
|
|
1080
|
+
}),
|
|
1081
|
+
"utf8",
|
|
1082
|
+
)
|
|
1083
|
+
.digest("hex");
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function fieldEvolutionRules(
|
|
1087
|
+
field: Field,
|
|
1088
|
+
prior: StableFieldId | undefined,
|
|
1089
|
+
aggregateSid: string,
|
|
1090
|
+
fieldSid: string,
|
|
1091
|
+
target: { contract: string; shape: unknown },
|
|
1092
|
+
driver: unknown,
|
|
1093
|
+
targetSchema: string,
|
|
1094
|
+
): StableFieldEvolution[] {
|
|
1095
|
+
const bySource = new Map<string, StableFieldEvolution>();
|
|
1096
|
+
const add = (rule: StableFieldEvolution, origin: string) => {
|
|
1097
|
+
if (rule.from === rule.to) return;
|
|
1098
|
+
const existing = bySource.get(rule.from);
|
|
1099
|
+
if (existing !== undefined && canonicalJson(existing) !== canonicalJson(rule)) {
|
|
1100
|
+
throw new Error(
|
|
1101
|
+
`field evolution collision for source contract ${rule.from}: ${origin} disagrees with an existing route. ` +
|
|
1102
|
+
`One source contract must have exactly one deterministic destination.`,
|
|
1103
|
+
);
|
|
1104
|
+
}
|
|
1105
|
+
bySource.set(rule.from, rule);
|
|
1106
|
+
};
|
|
1107
|
+
|
|
1108
|
+
for (const rule of prior?.evolutions ?? []) add(rule, "the prior release");
|
|
1109
|
+
|
|
1110
|
+
for (const declaration of field.evolutions ?? []) {
|
|
1111
|
+
const source = fieldValueContractDetails(declaration.source);
|
|
1112
|
+
const sourceSchema = fieldSchemaIdentity(
|
|
1113
|
+
aggregateSid,
|
|
1114
|
+
fieldSid,
|
|
1115
|
+
declaration.source.kind,
|
|
1116
|
+
source.contract,
|
|
1117
|
+
driver,
|
|
1118
|
+
);
|
|
1119
|
+
if (declaration.mode === "identity" && !safeIdentityProjection(source.shape, target.shape)) {
|
|
1120
|
+
throw new Error(
|
|
1121
|
+
`.evolvesFrom(source) is not a safe identity widening: some source values are not valid ` +
|
|
1122
|
+
`under the target field. Supply a finite business transform as the second argument.`,
|
|
1123
|
+
);
|
|
1124
|
+
}
|
|
1125
|
+
add(
|
|
1126
|
+
{
|
|
1127
|
+
from: sourceSchema,
|
|
1128
|
+
to: targetSchema,
|
|
1129
|
+
source: {
|
|
1130
|
+
kind: declaration.source.kind,
|
|
1131
|
+
...(declaration.source.mapValueKind !== undefined
|
|
1132
|
+
? { mapValueKind: declaration.source.mapValueKind }
|
|
1133
|
+
: {}),
|
|
1134
|
+
},
|
|
1135
|
+
mode: declaration.mode,
|
|
1136
|
+
...(declaration.cases !== undefined ? { cases: declaration.cases } : {}),
|
|
1137
|
+
},
|
|
1138
|
+
".evolvesFrom()",
|
|
1139
|
+
);
|
|
1140
|
+
}
|
|
1141
|
+
|
|
1142
|
+
// The compiler silently owns genuinely compatible widening between consecutive releases.
|
|
1143
|
+
// The prior lock carries the full value descriptor, so this is a proof, not a cast.
|
|
1144
|
+
if (
|
|
1145
|
+
prior?.schema !== undefined &&
|
|
1146
|
+
prior.schema !== targetSchema &&
|
|
1147
|
+
prior.shape !== undefined &&
|
|
1148
|
+
canonicalJson(prior.driver) === canonicalJson(driver) &&
|
|
1149
|
+
safeAutomaticWidening(prior.shape, target.shape) &&
|
|
1150
|
+
!bySource.has(prior.schema)
|
|
1151
|
+
) {
|
|
1152
|
+
add(
|
|
1153
|
+
{
|
|
1154
|
+
from: prior.schema,
|
|
1155
|
+
to: targetSchema,
|
|
1156
|
+
source: {
|
|
1157
|
+
kind: prior.kind ?? field.kind,
|
|
1158
|
+
...((prior.shape as { mapValueKind?: string } | undefined)?.mapValueKind !== undefined
|
|
1159
|
+
? { mapValueKind: (prior.shape as { mapValueKind: string }).mapValueKind }
|
|
1160
|
+
: {}),
|
|
1161
|
+
},
|
|
1162
|
+
mode: "identity",
|
|
1163
|
+
},
|
|
1164
|
+
"the compiler's compatible-widening proof",
|
|
1165
|
+
);
|
|
1166
|
+
}
|
|
1167
|
+
|
|
1168
|
+
// Every retained source route must reach the one current target. This is the compile-time
|
|
1169
|
+
// completeness gate for all still-supported eras, not merely the immediately prior law.
|
|
1170
|
+
for (const source of bySource.keys()) {
|
|
1171
|
+
const seen = new Set<string>();
|
|
1172
|
+
let cursor = source;
|
|
1173
|
+
while (cursor !== targetSchema) {
|
|
1174
|
+
if (seen.has(cursor)) {
|
|
1175
|
+
throw new Error(`field evolution graph cycles at ${cursor}; every source must terminate at ${targetSchema}`);
|
|
1176
|
+
}
|
|
1177
|
+
seen.add(cursor);
|
|
1178
|
+
const next = bySource.get(cursor);
|
|
1179
|
+
if (next === undefined) {
|
|
1180
|
+
throw new Error(
|
|
1181
|
+
`field evolution route from ${source} stops at ${cursor}, not the current contract ${targetSchema}. ` +
|
|
1182
|
+
`Carry the intermediate source with .evolvesFrom(...) before this rollout.`,
|
|
1183
|
+
);
|
|
1184
|
+
}
|
|
1185
|
+
cursor = next.to;
|
|
1186
|
+
}
|
|
1187
|
+
}
|
|
1188
|
+
|
|
1189
|
+
return [...bySource.values()].sort((a, b) => (a.from < b.from ? -1 : a.from > b.from ? 1 : 0));
|
|
1190
|
+
}
|
|
1191
|
+
|
|
1192
|
+
/** Explicit identity projection: the tenant declares the business meaning is continuous;
|
|
1193
|
+
* the compiler still proves every old value is accepted and its physical decoding is unchanged. */
|
|
1194
|
+
function safeIdentityProjection(sourceShape: unknown, targetShape: unknown): boolean {
|
|
1195
|
+
if (sourceShape === null || targetShape === null || typeof sourceShape !== "object" || typeof targetShape !== "object") {
|
|
1196
|
+
return canonicalJson(sourceShape) === canonicalJson(targetShape);
|
|
1197
|
+
}
|
|
1198
|
+
const source = sourceShape as Record<string, unknown>;
|
|
1199
|
+
const target = targetShape as Record<string, unknown>;
|
|
1200
|
+
const physical = (shape: Record<string, unknown>) => {
|
|
1201
|
+
const { kind, mapValueKind, jsonShape, encrypted } = shape;
|
|
1202
|
+
return { kind, mapValueKind, jsonShape, encrypted };
|
|
1203
|
+
};
|
|
1204
|
+
return canonicalJson(physical(source)) === canonicalJson(physical(target))
|
|
1205
|
+
&& jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
/** Automatic widening is stricter: every non-schema business descriptor must remain
|
|
1209
|
+
* identical. Brands, units, references and null meaning require an explicit declaration. */
|
|
1210
|
+
function safeAutomaticWidening(sourceShape: unknown, targetShape: unknown): boolean {
|
|
1211
|
+
if (sourceShape === null || targetShape === null || typeof sourceShape !== "object" || typeof targetShape !== "object") {
|
|
1212
|
+
return canonicalJson(sourceShape) === canonicalJson(targetShape);
|
|
1213
|
+
}
|
|
1214
|
+
const source = sourceShape as Record<string, unknown>;
|
|
1215
|
+
const target = targetShape as Record<string, unknown>;
|
|
1216
|
+
const metadata = (shape: Record<string, unknown>) => {
|
|
1217
|
+
const { jsonSchema: _jsonSchema, ...rest } = shape;
|
|
1218
|
+
return rest;
|
|
1219
|
+
};
|
|
1220
|
+
return canonicalJson(metadata(source)) === canonicalJson(metadata(target))
|
|
1221
|
+
&& jsonSchemaSubset(source.jsonSchema, target.jsonSchema);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
function jsonSchemaSubset(source: unknown, target: unknown): boolean {
|
|
1225
|
+
if (canonicalJson(source) === canonicalJson(target)) return true;
|
|
1226
|
+
if (source === null || target === null || typeof source !== "object" || typeof target !== "object") return false;
|
|
1227
|
+
const s = source as Record<string, unknown>;
|
|
1228
|
+
const t = target as Record<string, unknown>;
|
|
1229
|
+
if (Array.isArray(s.enum)) return s.enum.every((value) => jsonSchemaAcceptsLiteral(t, value));
|
|
1230
|
+
if (Object.prototype.hasOwnProperty.call(s, "const")) return jsonSchemaAcceptsLiteral(t, s.const);
|
|
1231
|
+
const sUnion = (Array.isArray(s.anyOf) ? s.anyOf : Array.isArray(s.oneOf) ? s.oneOf : undefined);
|
|
1232
|
+
if (sUnion !== undefined) return sUnion.every((arm) => jsonSchemaSubset(arm, target));
|
|
1233
|
+
const tUnion = (Array.isArray(t.anyOf) ? t.anyOf : Array.isArray(t.oneOf) ? t.oneOf : undefined);
|
|
1234
|
+
if (tUnion !== undefined) return tUnion.some((arm) => jsonSchemaSubset(source, arm));
|
|
1235
|
+
const sourceTypes = new Set(Array.isArray(s.type) ? s.type : s.type === undefined ? [] : [s.type]);
|
|
1236
|
+
const targetTypes = new Set(Array.isArray(t.type) ? t.type : t.type === undefined ? [] : [t.type]);
|
|
1237
|
+
if ([...sourceTypes].some((type) => !targetTypes.has(type))) return false;
|
|
1238
|
+
if (sourceTypes.has("object")) {
|
|
1239
|
+
const sp = (s.properties ?? {}) as Record<string, unknown>;
|
|
1240
|
+
const tp = (t.properties ?? {}) as Record<string, unknown>;
|
|
1241
|
+
const sr = new Set(Array.isArray(s.required) ? s.required as string[] : []);
|
|
1242
|
+
const tr = new Set(Array.isArray(t.required) ? t.required as string[] : []);
|
|
1243
|
+
if ([...tr].some((name) => !sr.has(name))) return false;
|
|
1244
|
+
for (const [name, schema] of Object.entries(sp)) {
|
|
1245
|
+
if (tp[name] === undefined) {
|
|
1246
|
+
if (t.additionalProperties === false) return false;
|
|
1247
|
+
} else if (!jsonSchemaSubset(schema, tp[name])) {
|
|
1248
|
+
return false;
|
|
1249
|
+
}
|
|
1250
|
+
}
|
|
1251
|
+
if (s.additionalProperties !== false && t.additionalProperties === false) return false;
|
|
1252
|
+
}
|
|
1253
|
+
if (sourceTypes.has("array") && s.items !== undefined && t.items !== undefined) {
|
|
1254
|
+
if (!jsonSchemaSubset(s.items, t.items)) return false;
|
|
1255
|
+
}
|
|
1256
|
+
return sourceTypes.size > 0 && [...sourceTypes].every((type) => targetTypes.has(type));
|
|
1257
|
+
}
|
|
1258
|
+
|
|
1259
|
+
function jsonSchemaAcceptsLiteral(schema: Record<string, unknown>, value: unknown): boolean {
|
|
1260
|
+
if (Array.isArray(schema.enum)) return schema.enum.some((candidate) => canonicalJson(candidate) === canonicalJson(value));
|
|
1261
|
+
if (Object.prototype.hasOwnProperty.call(schema, "const")) return canonicalJson(schema.const) === canonicalJson(value);
|
|
1262
|
+
const union = Array.isArray(schema.anyOf) ? schema.anyOf : Array.isArray(schema.oneOf) ? schema.oneOf : undefined;
|
|
1263
|
+
if (union !== undefined) return union.some((arm) => arm !== null && typeof arm === "object" && jsonSchemaAcceptsLiteral(arm as Record<string, unknown>, value));
|
|
1264
|
+
const actual = value === null ? "null" : Array.isArray(value) ? "array" : typeof value === "number" ? "number" : typeof value;
|
|
1265
|
+
const types = new Set(Array.isArray(schema.type) ? schema.type : schema.type === undefined ? [] : [schema.type]);
|
|
1266
|
+
return types.has(actual) || (actual === "number" && types.has("integer") && Number.isInteger(value));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
/** JSON Schema contains a handful of set-valued arrays whose authored order is not part
|
|
1270
|
+
* of the accepted value language. Normalize those (and VO enum `values`) so a cosmetic
|
|
1271
|
+
* enum/union reorder does not manufacture an N/N+1 incompatibility. */
|
|
1272
|
+
function normalizeFieldContract(value: unknown, parentKey = ""): unknown {
|
|
1273
|
+
if (Array.isArray(value)) {
|
|
1274
|
+
const items = value.map((item) => normalizeFieldContract(item));
|
|
1275
|
+
if (["enum", "required", "type", "values", "oneOf", "anyOf", "allOf"].includes(parentKey)) {
|
|
1276
|
+
return items.sort((a, b) => {
|
|
1277
|
+
const left = canonicalJson(a);
|
|
1278
|
+
const right = canonicalJson(b);
|
|
1279
|
+
return left < right ? -1 : left > right ? 1 : 0;
|
|
1280
|
+
});
|
|
1281
|
+
}
|
|
1282
|
+
return items;
|
|
1283
|
+
}
|
|
1284
|
+
if (value !== null && typeof value === "object") {
|
|
1285
|
+
return Object.fromEntries(
|
|
1286
|
+
Object.entries(value as Record<string, unknown>).map(([key, child]) => [
|
|
1287
|
+
key,
|
|
1288
|
+
normalizeFieldContract(child, key),
|
|
1289
|
+
]),
|
|
1290
|
+
);
|
|
1291
|
+
}
|
|
1292
|
+
return value;
|
|
1293
|
+
}
|
|
1294
|
+
|
|
1034
1295
|
/**
|
|
1035
1296
|
* Build the canonical semantic manifest for a domain module — purely from the
|
|
1036
1297
|
* in-memory DSL objects. No file reads, no bundle, no esbuild: the manifest is a
|
|
1037
1298
|
* function of the module alone, so it is bundler- AND location-independent.
|
|
1038
1299
|
*/
|
|
1039
1300
|
export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
1301
|
+
for (const agg of mod.aggregates) {
|
|
1302
|
+
if (agg.audience === undefined) {
|
|
1303
|
+
throw new Error(
|
|
1304
|
+
`aggregate '${agg.id}' has no audience. Nomos will not guess who may read business state: ` +
|
|
1305
|
+
`finish the declaration with .owned(), .readableBy("<business relation>"), or the deliberate ` +
|
|
1306
|
+
`.public() exception.`,
|
|
1307
|
+
);
|
|
1308
|
+
}
|
|
1309
|
+
}
|
|
1310
|
+
for (const directive of mod.directives) {
|
|
1311
|
+
const shape = zodObjectShape(directive.payloadSchema as ZodLike);
|
|
1312
|
+
if (shape === undefined) continue;
|
|
1313
|
+
for (const reserved of [
|
|
1314
|
+
OWNED_KEY_PAYLOAD_FIELD,
|
|
1315
|
+
OWNED_CUSTODY_PAYLOAD_FIELD,
|
|
1316
|
+
OWNED_PREMINTED_RECORDS_PAYLOAD_FIELD,
|
|
1317
|
+
BUSINESS_BIRTH_SEED_PAYLOAD_FIELD,
|
|
1318
|
+
]) {
|
|
1319
|
+
if (!Object.prototype.hasOwnProperty.call(shape, reserved)) continue;
|
|
1320
|
+
const frameworkBirthCarrier = reserved === BUSINESS_BIRTH_SEED_PAYLOAD_FIELD
|
|
1321
|
+
&& (directive as { businessBirth?: unknown }).businessBirth !== undefined;
|
|
1322
|
+
if (frameworkBirthCarrier) continue;
|
|
1323
|
+
throw new Error(
|
|
1324
|
+
`directive '${directive.id}': payload field '${reserved}' is reserved by Nomos ownership. ` +
|
|
1325
|
+
`Sorry mate, that concept is already taken by the framework; give the business payload field ` +
|
|
1326
|
+
`a different name.`,
|
|
1327
|
+
);
|
|
1328
|
+
}
|
|
1329
|
+
}
|
|
1040
1330
|
const aggregates: CanonicalAggregate[] = mod.aggregates
|
|
1041
1331
|
.map((agg) => {
|
|
1042
|
-
// SECURE-BY-DEFAULT — `.owned()` is declared in the audience API, but its owner-SCOPED reads
|
|
1043
|
-
// (the projection row-filter) + per-record E2E are not yet enforced. Refuse to compile owned law
|
|
1044
|
-
// FAIL-CLOSED rather than ship a type whose reads aren't gated (architecture/secure_by_default_dsl.md
|
|
1045
|
-
// build order). Use `.public()` or `.readableBy(cap)` until the `.owned()` slice lands.
|
|
1046
|
-
if (agg.audience?.kind === "owned") {
|
|
1047
|
-
throw new Error(
|
|
1048
|
-
`aggregate '${agg.id}': .owned() is declared but its read-scoping + E2E are not yet enforced — ` +
|
|
1049
|
-
`refusing to compile owned law (fail-closed). Use .public() or .readableBy("<cap>") for now ` +
|
|
1050
|
-
`(the .owned() slice is next — see architecture/secure_by_default_dsl.md).`,
|
|
1051
|
-
);
|
|
1052
|
-
}
|
|
1053
1332
|
return {
|
|
1054
1333
|
id: agg.id,
|
|
1055
1334
|
schema: sortedSchema(encodeKernelSchema(agg)),
|
|
@@ -1065,6 +1344,10 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
1065
1344
|
// E2E ENCRYPTION (slice 2) — the encrypted field names. Hash-bearing law;
|
|
1066
1345
|
// omit-when-none so plaintext aggregates are byte-stable.
|
|
1067
1346
|
...(agg.encryptedFields !== undefined ? { encryptedFields: [...agg.encryptedFields] } : {}),
|
|
1347
|
+
...(agg.audience?.kind === "owned"
|
|
1348
|
+
? { owned: { ownerField: agg.audience.ownerField, relation: OWNED_READ_RELATION } }
|
|
1349
|
+
: {}),
|
|
1350
|
+
...(agg.internalFields !== undefined ? { internalFields: [...agg.internalFields].sort() } : {}),
|
|
1068
1351
|
// ATTRIBUTED READS — the attributed field names (the driver wrap in `schema` is
|
|
1069
1352
|
// the hash-bearing law; this mirror is what codegen reads). Omit-when-none.
|
|
1070
1353
|
...(agg.attributedFields !== undefined ? { attributedFields: [...agg.attributedFields] } : {}),
|
|
@@ -1072,6 +1355,9 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
1072
1355
|
})
|
|
1073
1356
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1074
1357
|
|
|
1358
|
+
const conflictResolutionByDirective = new Map(
|
|
1359
|
+
(mod.surfacedResolvers ?? []).map((binding) => [binding.directiveId, binding] as const),
|
|
1360
|
+
);
|
|
1075
1361
|
const directives: CanonicalDirective[] = mod.directives
|
|
1076
1362
|
.map((d) => ({
|
|
1077
1363
|
id: d.id,
|
|
@@ -1085,9 +1371,16 @@ export function domainManifest(mod: DomainModule): CanonicalManifest {
|
|
|
1085
1371
|
...canonicalReads(d.declaredReads),
|
|
1086
1372
|
...canonicalCapturedReads(d.declaredQueryReads ?? []),
|
|
1087
1373
|
...canonicalEmits(d.declaredEmits),
|
|
1088
|
-
...canonicalCertifiedReads(d.declaredCertifiedReads),
|
|
1089
1374
|
...canonicalRelations(d.declaredRelations),
|
|
1090
1375
|
...canonicalHostCapabilities(d.declaredHostCapabilities),
|
|
1376
|
+
...(conflictResolutionByDirective.has(d.id)
|
|
1377
|
+
? {
|
|
1378
|
+
conflictResolution: {
|
|
1379
|
+
aggregate: conflictResolutionByDirective.get(d.id)!.aggregateId,
|
|
1380
|
+
field: conflictResolutionByDirective.get(d.id)!.field,
|
|
1381
|
+
},
|
|
1382
|
+
}
|
|
1383
|
+
: {}),
|
|
1091
1384
|
}))
|
|
1092
1385
|
.sort((a, b) => (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
1093
1386
|
|
|
@@ -61,6 +61,14 @@ export function qualifyManifestAggregateLabels(
|
|
|
61
61
|
directives: manifest.directives.map((d) => ({
|
|
62
62
|
...d,
|
|
63
63
|
...(namespaced.has(d.target) ? { target: qa(d.target) } : {}),
|
|
64
|
+
...(d.conflictResolution !== undefined && namespaced.has(d.conflictResolution.aggregate)
|
|
65
|
+
? {
|
|
66
|
+
conflictResolution: {
|
|
67
|
+
...d.conflictResolution,
|
|
68
|
+
aggregate: qa(d.conflictResolution.aggregate),
|
|
69
|
+
},
|
|
70
|
+
}
|
|
71
|
+
: {}),
|
|
64
72
|
...(d.capturedReads !== undefined
|
|
65
73
|
? {
|
|
66
74
|
capturedReads: d.capturedReads.map((r) =>
|
|
@@ -72,10 +72,14 @@ export const GLOBAL_READ_PROJECTION_AGGREGATE_IDS: ReadonlySet<string> = new Set
|
|
|
72
72
|
// domain lifecycle (the law records themselves)
|
|
73
73
|
"PolicyBundle",
|
|
74
74
|
"DomainInstallation",
|
|
75
|
+
"nomos_internal_ReleaseReceipt_6f0d3a",
|
|
75
76
|
// encryption identity + scope (the operator-blind key machinery)
|
|
76
77
|
"EncKey",
|
|
77
78
|
"KeyScope",
|
|
78
79
|
"WrappedKey",
|
|
80
|
+
// framework-owned private-data recovery policy + durable recovery order
|
|
81
|
+
"NomosOwnedRecoveryPolicy",
|
|
82
|
+
"NomosOwnedRecoveryOrder",
|
|
79
83
|
// genesis / births
|
|
80
84
|
"GenesisWarrant",
|
|
81
85
|
// metered birth (slice A) — NomosBirthRecord is kernel-read by the birth-record
|
package/src/relation.ts
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
// the source/target aggregate endpoints, the BOUNDED evidence read it discloses (the
|
|
11
11
|
// disclosure contract + anti-smuggle key, §2.4), and that an invariant predicate guards it.
|
|
12
12
|
//
|
|
13
|
-
// This is DECLARATION ONLY — symmetric with `emits`/`requires`/`
|
|
13
|
+
// This is DECLARATION ONLY — symmetric with `emits`/`requires`/`reads`. It lowers
|
|
14
14
|
// into the domain manifest IR (`manifest.ts`, the hash-pinned #136 identity), which the ONE
|
|
15
15
|
// write-path gate (`executor::admit` step 5.5 / `kernel::manifest_view`) resolves to adjudicate
|
|
16
16
|
// a carried PR against the target's OWN law. The invariant PREDICATE itself is executable
|