@githolon/dsl 0.100.6 → 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 +6 -0
- package/dart/lib/src/dev_cloud.dart +46 -15
- package/dart/lib/src/test_harness.dart +78 -16
- package/dart/lib/src/webview_bridge.dart +640 -126
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/bundle_version_pairing_test.dart +11 -22
- package/dart/test/co2_product_gauntlet_test.dart +228 -0
- package/dart/test/errors_test.dart +63 -12
- package/dart/test/first_app_gauntlet_test.dart +29 -3
- 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 +9 -1
- package/src/births.ts +53 -22
- package/src/build_package.ts +55 -14
- package/src/business_birth.ts +48 -7
- package/src/codegen_dart.ts +267 -37
- package/src/codegen_dart_client.ts +921 -82
- 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 +155 -45
- 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 +47 -2
- 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/directive.ts
CHANGED
|
@@ -20,7 +20,6 @@ import type { AggregateHandle, ReadModelRow } from "./aggregate.js";
|
|
|
20
20
|
import type { Field } from "./fields.js";
|
|
21
21
|
import type { PlannedOp } from "./ops.js";
|
|
22
22
|
import type { Ports } from "./ctx.js";
|
|
23
|
-
import type { CertifiedReadDecl } from "./certified_read.js";
|
|
24
23
|
import type { RelationDecl } from "./relation.js";
|
|
25
24
|
import type { QueryDecl } from "./query.js";
|
|
26
25
|
import { normalizeActivity, type ActivityInput, type ActivityDescriptor } from "./activity.js";
|
|
@@ -117,12 +116,14 @@ export interface Directive<P = unknown> {
|
|
|
117
116
|
*/
|
|
118
117
|
readonly description?: string;
|
|
119
118
|
/**
|
|
120
|
-
*
|
|
119
|
+
* Optional user-facing ACTIVITY polish (`architecture/activity_descriptors.md`) — the kind/title/message
|
|
121
120
|
* template, refs, params, category/icon/severity a client uses to render a typed activity row from this
|
|
122
121
|
* directive's intents (no string-enricher). Set by `.activity({...})`; NON-LAW, additive (same class as
|
|
123
122
|
* `description` — a directive that never declares activity is byte-identical). Normalized + validated at
|
|
124
|
-
* author time. `undefined` ⇒
|
|
125
|
-
*
|
|
123
|
+
* author time. `undefined` ⇒ codegen supplies the framework-owned baseline row from this directive's
|
|
124
|
+
* business name, declared effects and verified author/time; the change never disappears from the story.
|
|
125
|
+
* (The setter is `.activity({...})`; this stored field is `activityDescriptor` to avoid a method/field
|
|
126
|
+
* name clash on `RequirableDirective`.)
|
|
126
127
|
*/
|
|
127
128
|
readonly activityDescriptor?: ActivityDescriptor;
|
|
128
129
|
/**
|
|
@@ -258,19 +259,6 @@ export interface Directive<P = unknown> {
|
|
|
258
259
|
* (declares no emits). Gate-side enforcement (emitted ⊆ declared) is a later step.
|
|
259
260
|
*/
|
|
260
261
|
readonly declaredEmits: Record<string, { max?: number }>;
|
|
261
|
-
/**
|
|
262
|
-
* The directive's DECLARED CertifiedReads (survival Constraint 5; `certified_read.md`
|
|
263
|
-
* §"Tenant authoring"): the profiled, named write-path reads its `decide()` consults — the
|
|
264
|
-
* `reads:{ name: q.someProfiledRead(args) }` shape, keyed by the LOCAL binding name. Each
|
|
265
|
-
* value is a profile-conforming {@link CertifiedReadDecl} (compiled through SQL-profile-v0 at
|
|
266
|
-
* `certifiedRead(...).sql(...)`, so an under-specified read is refused at COMPILE). The
|
|
267
|
-
* declared `query_id`s LAND IN THE CERTIFIED MANIFEST (`manifest.ts`), so the ONE write-path
|
|
268
|
-
* gate (`executor::admit` step 5.6) DERIVES the expected declared-read set and REFUSES any
|
|
269
|
-
* admit where a declared read produced no witness (the vacuous-pass close). Defaults to `{}`
|
|
270
|
-
* (declares no write-path reads) — OMITTED ENTIRELY from the manifest when empty, so a
|
|
271
|
-
* read-free directive's canonical manifest is byte-identical to before this existed.
|
|
272
|
-
*/
|
|
273
|
-
readonly declaredCertifiedReads: Record<string, CertifiedReadDecl>;
|
|
274
262
|
/**
|
|
275
263
|
* The directive's DECLARED cross-workspace relations (`cross_workspace.md` §2): the relations
|
|
276
264
|
* a cross-workspace PR proposing THIS directive is adjudicated against. The TARGET domain
|
|
@@ -480,19 +468,6 @@ export interface RequirableDirective<P> extends Directive<P> {
|
|
|
480
468
|
* var / missing payload field refuse). Non-law, additive; a directive that never calls it is byte-identical.
|
|
481
469
|
*/
|
|
482
470
|
activity(input: ActivityInput): RequirableDirective<P>;
|
|
483
|
-
/**
|
|
484
|
-
* Declare the directive's CertifiedReads (survival Constraint 5; `certified_read.md`
|
|
485
|
-
* §"Tenant authoring") — the profiled, named write-path reads its `decide()` consults, the
|
|
486
|
-
* `reads:{ name: q.someProfiledRead(args) }` shape. Each value is a {@link CertifiedReadDecl}
|
|
487
|
-
* (built via `certifiedRead("id").sql("…", {multiRow,uniqueTieBreakers})`, which refuses an
|
|
488
|
-
* under-specified read at COMPILE through SQL-profile-v0). The declared `query_id`s LAND IN
|
|
489
|
-
* THE CERTIFIED MANIFEST, so the ONE write-path gate DERIVES the required-read set and refuses
|
|
490
|
-
* any admit where a declared read produced no witness (the vacuous-pass close). Callable
|
|
491
|
-
* multiple times to accumulate (a repeated local name's later decl wins). Purely additive +
|
|
492
|
-
* non-destructive — a directive that never calls this declares no write-path reads (`{}`) and
|
|
493
|
-
* is byte-identical in the manifest to before this existed. Returns a NEW requirable directive.
|
|
494
|
-
*/
|
|
495
|
-
readsCertified(reads: Record<string, CertifiedReadDecl>): RequirableDirective<P>;
|
|
496
471
|
/**
|
|
497
472
|
* Declare a cross-workspace relation (`cross_workspace.md` §2) this directive (the TARGET of a
|
|
498
473
|
* cross-workspace PR's `proposes`) is adjudicated against — built via
|
|
@@ -697,7 +672,6 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
697
672
|
declaredReads: [],
|
|
698
673
|
declaredQueryReads: [],
|
|
699
674
|
declaredEmits: {},
|
|
700
|
-
declaredCertifiedReads: {},
|
|
701
675
|
declaredRelations: [],
|
|
702
676
|
declaredHostCapabilities: [],
|
|
703
677
|
// `.withId(...)` — record the caller-supplied-id extractor (codegen/authoring hint). The `absent`
|
|
@@ -935,13 +909,6 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
|
|
|
935
909
|
];
|
|
936
910
|
return makeRequirable({ ...base, activityDescriptor: normalizeActivity(base.id, input, base.payloadSchema, createdIds) });
|
|
937
911
|
},
|
|
938
|
-
readsCertified(reads: Record<string, CertifiedReadDecl>): RequirableDirective<P> {
|
|
939
|
-
// Accumulate onto any prior declared CertifiedReads (a repeated local name's later decl
|
|
940
|
-
// wins) — additive, non-destructive. The local binding NAME (the key) is what `decide`
|
|
941
|
-
// reads via `reads.<name>`; the decl's `queryId` is the manifest-landed identity.
|
|
942
|
-
const merged: Record<string, CertifiedReadDecl> = { ...base.declaredCertifiedReads, ...reads };
|
|
943
|
-
return makeRequirable({ ...base, declaredCertifiedReads: merged });
|
|
944
|
-
},
|
|
945
912
|
declaresRelation(rel: RelationDecl): RequirableDirective<P> {
|
|
946
913
|
// Accumulate onto any prior relations — additive, non-destructive. A relation lands in
|
|
947
914
|
// the manifest keyed by its `proposes` directive (cross_workspace.md §3.2.1).
|
package/src/drivers.ts
CHANGED
|
@@ -23,6 +23,7 @@ import type { WireDriver } from "./wire.js";
|
|
|
23
23
|
*/
|
|
24
24
|
export type KernelDriverKind =
|
|
25
25
|
| "Lww"
|
|
26
|
+
| "FirstWriterWins"
|
|
26
27
|
// ── EXPLICIT TIME-AUTHORITY Lww drivers (the "whose clock?" axis) ──
|
|
27
28
|
// Nomos has no servers, so "do we trust the clock?" is the WRONG question — the real
|
|
28
29
|
// one is WHOSE clock orders THIS field, and a dev assigns it per field. `Lww` trusts
|
|
@@ -57,6 +58,9 @@ export interface Driver {
|
|
|
57
58
|
}
|
|
58
59
|
|
|
59
60
|
export const Lww: Driver = { kind: "Lww" };
|
|
61
|
+
/** Nomos's ordinary scalar conflict policy: an informed later edit replaces the
|
|
62
|
+
* value; concurrent edits converge on the earliest writer. */
|
|
63
|
+
export const FirstWriterWins: Driver = { kind: "FirstWriterWins" };
|
|
60
64
|
export const AddWins: Driver = { kind: "AddWins" };
|
|
61
65
|
export const Conflict: Driver = { kind: "Conflict" };
|
|
62
66
|
export const MapOf = (inner: Driver): Driver => ({ kind: "MapOf", inner });
|
|
@@ -97,9 +101,10 @@ export const LastPosition: Driver = { kind: "LastPosition" };
|
|
|
97
101
|
export const RemoveWins: Driver = { kind: "RemoveWins" };
|
|
98
102
|
export const Counter: Driver = { kind: "Counter" };
|
|
99
103
|
|
|
100
|
-
/** The
|
|
101
|
-
*
|
|
104
|
+
/** The scalar inner drivers `Surfaced` may wrap in v1. The pending set preserves competing
|
|
105
|
+
* business versions until a resolver produces one merged result. */
|
|
102
106
|
const SURFACED_INNER_KINDS: ReadonlySet<DriverKind> = new Set<DriverKind>([
|
|
107
|
+
"FirstWriterWins",
|
|
103
108
|
"Lww",
|
|
104
109
|
"LwwCausal",
|
|
105
110
|
"LwwBy",
|
|
@@ -114,7 +119,8 @@ const SURFACED_INNER_KINDS: ReadonlySet<DriverKind> = new Set<DriverKind>([
|
|
|
114
119
|
* competing values as a bounded pending-observation set (cap 8, deterministically
|
|
115
120
|
* pruned) that a read surfaces as `{"__surfaced":{"pending":[{value, author,
|
|
116
121
|
* occurredAt}, ...]}}` — ALWAYS 2+ competing observations (a single-observation
|
|
117
|
-
* set read-collapses to the plain scalar) — so an app can
|
|
122
|
+
* set read-collapses to the plain scalar) — so an app can explain the competing versions
|
|
123
|
+
* and produce a merged business result.
|
|
118
124
|
* Linear writes and
|
|
119
125
|
* fast-forwards behave EXACTLY as the inner driver (`Surfaced(LwwBy(p))` keeps
|
|
120
126
|
* `p`'s write filter); pending sets form ONLY under real concurrency.
|
|
@@ -139,7 +145,7 @@ export const Surfaced = (inner: Driver): Driver => {
|
|
|
139
145
|
if (!SURFACED_INNER_KINDS.has(inner.kind)) {
|
|
140
146
|
throw new Error(
|
|
141
147
|
`Surfaced(${inner.kind}) is not supported — Surfaced wraps an Lww-class SCALAR inner driver only ` +
|
|
142
|
-
`(Lww, LwwCausal, LwwBy) in v1. A pending set holds competing scalar
|
|
148
|
+
`(FirstWriterWins, Lww, LwwCausal, LwwBy) in v1. A pending set holds competing scalar versions until the business merges them; ` +
|
|
143
149
|
`'${inner.kind}' ${
|
|
144
150
|
inner.kind === "Surfaced"
|
|
145
151
|
? "is already Surfaced — nesting adds nothing"
|
|
@@ -160,6 +166,8 @@ const PALETTE_ONLY: ReadonlySet<DriverKind> = new Set<DriverKind>([]);
|
|
|
160
166
|
/** Encode one Driver to its kernel wire shape, or throw if not yet in the kernel. */
|
|
161
167
|
export function encodeDriver(d: Driver): WireDriver {
|
|
162
168
|
switch (d.kind) {
|
|
169
|
+
case "FirstWriterWins":
|
|
170
|
+
return "FirstWriterWins";
|
|
163
171
|
case "Lww":
|
|
164
172
|
return "Lww";
|
|
165
173
|
// ── EXPLICIT TIME-AUTHORITY Lww drivers (kernel `merge_field` arms) ──
|
package/src/engine_entry.ts
CHANGED
|
@@ -81,6 +81,8 @@ import type { InvariantBody, InvariantEvidence, InvariantVerdict } from "./relat
|
|
|
81
81
|
import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
|
|
82
82
|
import type { QueryRow } from "./report.js";
|
|
83
83
|
import { expandBusinessBirthExports } from "./business_birth.js";
|
|
84
|
+
import { withOwnedCustodyExports } from "./framework/owned_custody.js";
|
|
85
|
+
import { withBusinessCorrectionExports } from "./framework/business_corrections.js";
|
|
84
86
|
|
|
85
87
|
/** One bundled domain module: a bag of named exports the entry scans by SHAPE. */
|
|
86
88
|
export type DomainModuleExports = Record<string, unknown>;
|
|
@@ -249,6 +251,8 @@ function mergeModules(mods: readonly DomainModuleExports[], domain: string): Dom
|
|
|
249
251
|
let merged: DomainModuleExports = {};
|
|
250
252
|
for (const m of mods) merged = { ...merged, ...m };
|
|
251
253
|
merged = expandBusinessBirthExports(merged, domain);
|
|
254
|
+
merged = withOwnedCustodyExports(merged);
|
|
255
|
+
merged = withBusinessCorrectionExports(merged);
|
|
252
256
|
// `capability()` bundles expand into discoverable entries here — the engine
|
|
253
257
|
// plane's ONE merge point, so the registry sees the task aggregate + quartet
|
|
254
258
|
// without per-piece re-exports (capability_exports.ts).
|
package/src/evolve_preview.ts
CHANGED
|
@@ -7,19 +7,21 @@
|
|
|
7
7
|
* THE EVOLVE PREVIEW — `githolon check --against <prior>` (architecture/author_time_gate.md,
|
|
8
8
|
* the `--against` row of check slice 3). The evolve gate is the kernel's #58 diff-by-stable-id
|
|
9
9
|
* (executor/src/law58.rs::evolve_violation): a RENAME (same sid, new label) admits silently; a
|
|
10
|
-
* true REMOVAL /
|
|
11
|
-
*
|
|
10
|
+
* true REMOVAL / merge-driver change refuses TYPED unless the deploy JSON carries a
|
|
11
|
+
* `dispositions` acknowledgement. A changed BUSINESS VALUE CONTRACT is stricter: it needs
|
|
12
|
+
* deterministic `.evolvesFrom(...)` coverage and cannot be waived by an acknowledgement.
|
|
12
13
|
*
|
|
13
14
|
* This module is the AUTHOR-TIME TWIN of that gate: given the PRIOR law and the CURRENT build,
|
|
14
15
|
* it diffs them BY STABLE ID and pre-fires the gate's verdicts — with the EXACT generated
|
|
15
|
-
*
|
|
16
|
-
* pastes it into their deploy JSON and the real deploy admits. It classifies:
|
|
16
|
+
* repair inside the refusal's `fix`, matching what the real gate will accept. It classifies:
|
|
17
17
|
*
|
|
18
18
|
* RENAME (note) — same sid, new label; admits silently, old rows read the new label.
|
|
19
19
|
* ADDITIVE (note) — a new aggregate/field/directive (incl. an additive optional VO field);
|
|
20
20
|
* the evolve gate admits it silently (vo_first_class.md slice 6).
|
|
21
21
|
* REMOVAL (refuse) — a sid disappeared; the gate REFUSES without dispositions.retired:[sid].
|
|
22
|
-
*
|
|
22
|
+
* CONTRACT (refuse) — same sid, changed value language without a deterministic projection;
|
|
23
|
+
* needs `.evolvesFrom(...)`, never a disposition.
|
|
24
|
+
* DRIVER (refuse) — same sid, changed merge driver; needs dispositions.retyped.
|
|
23
25
|
* PAYLOAD (warn) — a directive's payload shape changed; clients compiled against the old
|
|
24
26
|
* law get typed 422s until regenerated (directives are not sid-tracked).
|
|
25
27
|
*
|
|
@@ -44,7 +46,15 @@ export interface EvolveAggregate {
|
|
|
44
46
|
readonly label: string;
|
|
45
47
|
/** keyed by FIELD SID. `driver` is the merge-law JSON (from the lockfile/usda schemas)
|
|
46
48
|
* when known — it lets the retype arm catch a driver-only change (same read kind). */
|
|
47
|
-
readonly fields: Record<string, {
|
|
49
|
+
readonly fields: Record<string, {
|
|
50
|
+
sid: string;
|
|
51
|
+
label: string;
|
|
52
|
+
kind: string;
|
|
53
|
+
driver?: string;
|
|
54
|
+
contract?: string;
|
|
55
|
+
schema?: string;
|
|
56
|
+
evolutions?: readonly { from: string; to: string }[];
|
|
57
|
+
}>;
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
/** One domain's evolve surface: aggregates (sid-keyed) + directive payload shapes (name-keyed). */
|
|
@@ -87,6 +97,11 @@ function aggregatesFromStableIds(
|
|
|
87
97
|
label: fLabel,
|
|
88
98
|
kind: f.kind ?? "",
|
|
89
99
|
...(driver !== undefined ? { driver: JSON.stringify(driver) } : {}),
|
|
100
|
+
...(f.contract !== undefined ? { contract: f.contract } : {}),
|
|
101
|
+
...(f.schema !== undefined ? { schema: f.schema } : {}),
|
|
102
|
+
...(f.evolutions !== undefined ? {
|
|
103
|
+
evolutions: f.evolutions.map((rule) => ({ from: rule.from, to: rule.to })),
|
|
104
|
+
} : {}),
|
|
90
105
|
};
|
|
91
106
|
}
|
|
92
107
|
out[a.sid] = { sid: a.sid, label: aggLabel, fields };
|
|
@@ -188,11 +203,23 @@ export function evolveLawFromDeployJson(deploy: DeployJson): EvolveLaw {
|
|
|
188
203
|
const usdDom = fromUsd[key];
|
|
189
204
|
if (usdDom !== undefined) {
|
|
190
205
|
for (const agg of Object.values(dom.aggregates)) {
|
|
191
|
-
const
|
|
192
|
-
|
|
206
|
+
const stableAggregate = Object.values(usdDom.stableIds.aggregates)
|
|
207
|
+
.find((candidate) => candidate.sid === agg.sid);
|
|
208
|
+
const stableFields = new Map(
|
|
209
|
+
Object.values(stableAggregate?.fields ?? {})
|
|
210
|
+
.map((field) => [field.sid, field] as const),
|
|
211
|
+
);
|
|
212
|
+
const schema = usdDom.schemas[agg.label]
|
|
213
|
+
?? Object.entries(usdDom.schemas).find(([type]) => type.endsWith(`.${agg.label}`))?.[1];
|
|
193
214
|
for (const f of Object.values(agg.fields)) {
|
|
194
|
-
const driver = schema[f.label];
|
|
215
|
+
const driver = schema?.[f.label];
|
|
195
216
|
if (driver !== undefined) (f as { driver?: string }).driver = JSON.stringify(driver);
|
|
217
|
+
const stable = stableFields.get(f.sid);
|
|
218
|
+
if (stable?.contract !== undefined) f.contract = stable.contract;
|
|
219
|
+
if (stable?.schema !== undefined) f.schema = stable.schema;
|
|
220
|
+
if (stable?.evolutions !== undefined) {
|
|
221
|
+
f.evolutions = stable.evolutions.map((rule) => ({ from: rule.from, to: rule.to }));
|
|
222
|
+
}
|
|
196
223
|
}
|
|
197
224
|
}
|
|
198
225
|
}
|
|
@@ -274,6 +301,28 @@ function ensureDisp(map: Record<string, EvolveDispositionsJson>, domain: string)
|
|
|
274
301
|
return (map[domain] ??= {});
|
|
275
302
|
}
|
|
276
303
|
|
|
304
|
+
/** Mirror executor::law58::evolution_reaches: one deterministic route per source,
|
|
305
|
+
* accumulated across supported releases, must terminate at the current schema. */
|
|
306
|
+
function evolutionReaches(
|
|
307
|
+
field: EvolveAggregate["fields"][string],
|
|
308
|
+
source: string | undefined,
|
|
309
|
+
target: string | undefined,
|
|
310
|
+
): boolean {
|
|
311
|
+
if (source === undefined || target === undefined) return false;
|
|
312
|
+
if (source === target) return true;
|
|
313
|
+
const bySource = new Map((field.evolutions ?? []).map((rule) => [rule.from, rule.to]));
|
|
314
|
+
const seen = new Set<string>();
|
|
315
|
+
let cursor = source;
|
|
316
|
+
while (cursor !== target) {
|
|
317
|
+
if (seen.has(cursor)) return false;
|
|
318
|
+
seen.add(cursor);
|
|
319
|
+
const next = bySource.get(cursor);
|
|
320
|
+
if (next === undefined) return false;
|
|
321
|
+
cursor = next;
|
|
322
|
+
}
|
|
323
|
+
return true;
|
|
324
|
+
}
|
|
325
|
+
|
|
277
326
|
/** Diff one domain's aggregates/fields/directives BY STABLE ID and append findings + dispositions. */
|
|
278
327
|
function diffDomain(
|
|
279
328
|
domain: string,
|
|
@@ -332,10 +381,22 @@ function diffDomain(
|
|
|
332
381
|
const kindChanged = oldF.kind !== "" && newF.kind !== "" && oldF.kind !== newF.kind;
|
|
333
382
|
const driverChanged =
|
|
334
383
|
oldF.driver !== undefined && newF.driver !== undefined && oldF.driver !== newF.driver;
|
|
335
|
-
|
|
384
|
+
const contractChanged =
|
|
385
|
+
oldF.contract !== undefined && newF.contract !== undefined && oldF.contract !== newF.contract;
|
|
386
|
+
const contractCovered = contractChanged && evolutionReaches(newF, oldF.schema, newF.schema);
|
|
387
|
+
if (contractChanged && !contractCovered) {
|
|
388
|
+
findings.push({
|
|
389
|
+
domain,
|
|
390
|
+
level: "refuse",
|
|
391
|
+
what: `field '${oldAgg.label}.${oldF.label}' (stable id ${fSid}) changes its business value contract without deterministic evolution coverage`,
|
|
392
|
+
fix: `declare .evolvesFrom(oldField) for a compatible widening or .evolvesFrom(oldField, value => nextValue) for an exhaustive finite business mapping; dispositions.retyped cannot waive this`,
|
|
393
|
+
});
|
|
394
|
+
continue;
|
|
395
|
+
}
|
|
396
|
+
if ((kindChanged && !contractCovered) || driverChanged) {
|
|
336
397
|
const d = ensureDisp(disp, domain);
|
|
337
398
|
(d.retyped ??= {})[fSid] = "<why goes here>";
|
|
338
|
-
const how = kindChanged
|
|
399
|
+
const how = kindChanged && !contractCovered
|
|
339
400
|
? `read kind ${oldF.kind} → ${newF.kind}`
|
|
340
401
|
: `merge driver ${oldF.driver} → ${newF.driver}`;
|
|
341
402
|
findings.push({
|