@githolon/dsl 0.71.0 → 0.72.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/lib/nomos_client.dart +4 -0
- package/dart/lib/src/session_bound.dart +309 -0
- package/dart/lib/src/test_harness.dart +136 -4
- package/dart/lib/src/webview_bridge.dart +161 -25
- package/dart/test/cluster_attested_test.dart +0 -1
- package/dart/test/session_bound_test.dart +255 -0
- package/dart_types/lib/nomos_types.dart +3 -0
- package/dart_types/lib/src/surfaced.dart +208 -0
- package/dart_types/test/surfaced_test.dart +69 -0
- package/package.json +1 -1
- package/src/aggregate.ts +64 -7
- package/src/build_package.ts +22 -0
- package/src/codegen_dart.ts +155 -31
- package/src/codegen_dart_client.ts +245 -28
- package/src/codegen_openapi.ts +20 -0
- package/src/codegen_proof.ts +5 -1
- package/src/codegen_ts.ts +113 -1
- package/src/compile_package_main.ts +120 -8
- package/src/directive.ts +40 -0
- package/src/drivers.ts +63 -2
- package/src/idof_lint.ts +144 -0
- package/src/index.ts +1 -0
- package/src/surfaced_lint.ts +0 -0
- package/src/usd_layers.ts +5 -0
- package/src/wire.ts +4 -1
package/src/codegen_dart.ts
CHANGED
|
@@ -491,6 +491,14 @@ interface AggFieldSpec {
|
|
|
491
491
|
nullAdmits?: boolean;
|
|
492
492
|
/** The law-documentation `reason` for a null-admitting field (surfaces in the Dart doc comment). */
|
|
493
493
|
nullReason?: string;
|
|
494
|
+
/**
|
|
495
|
+
* SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): the field is declared
|
|
496
|
+
* `.merge(Surfaced(...))`, so its read value is the base scalar OR the pending-conflict
|
|
497
|
+
* envelope — typed `SurfacedValue<T>?` (T = the base {@link dartType}) and decoded via
|
|
498
|
+
* `SurfacedValue.decode`. The FIRST driver-conditional generated typing; absent for every
|
|
499
|
+
* other field (byte-identical to a pre-Surfaced compile).
|
|
500
|
+
*/
|
|
501
|
+
surfaced?: true;
|
|
494
502
|
}
|
|
495
503
|
|
|
496
504
|
export interface DartImport {
|
|
@@ -546,7 +554,7 @@ function readModelNullableSuffix(s: AggFieldSpec): string {
|
|
|
546
554
|
return s.optional ? "?" : "";
|
|
547
555
|
}
|
|
548
556
|
|
|
549
|
-
function dartMemberName(raw: string): string {
|
|
557
|
+
export function dartMemberName(raw: string): string {
|
|
550
558
|
const parts = raw.split(/[^A-Za-z0-9]+/).filter((p) => p !== "");
|
|
551
559
|
const camel =
|
|
552
560
|
parts.length === 0
|
|
@@ -748,7 +756,7 @@ function aggFieldSpec(
|
|
|
748
756
|
// `contracts.dart`); the aggregate class is `<ContractId>` and must not collide, hence the suffix.
|
|
749
757
|
const target = field.foreignRef.targetIsContract === true
|
|
750
758
|
? `${field.foreignRef.targetAggregateId}Contract`
|
|
751
|
-
: field.foreignRef.targetAggregateId;
|
|
759
|
+
: concreteClassName(field.foreignRef.targetAggregateId);
|
|
752
760
|
return {
|
|
753
761
|
name,
|
|
754
762
|
wireName,
|
|
@@ -868,11 +876,19 @@ function aggregateFieldSpecs(
|
|
|
868
876
|
: f.isFromBirth === true
|
|
869
877
|
? { ...base, optional: false }
|
|
870
878
|
: { ...base, optional: true };
|
|
879
|
+
// SURFACED MERGE CONFLICTS (architecture/surfaced_merge_conflict.md): a `.merge(Surfaced(...))`
|
|
880
|
+
// field types `SurfacedValue<T>?` and decodes via `SurfacedValue.decode`. ALWAYS nullable —
|
|
881
|
+
// even `.fromBirth()` (the wrapper's pending arm has no non-null plain-scalar reading).
|
|
882
|
+
// aggregate() already v1-scoped the driver to Lww-class scalar kinds; excludes nullAdmits.
|
|
883
|
+
const surfacedSpec: AggFieldSpec =
|
|
884
|
+
(f.driver as { kind?: string } | undefined)?.kind === "Surfaced"
|
|
885
|
+
? { ...baseSpec, optional: true, surfaced: true }
|
|
886
|
+
: baseSpec;
|
|
871
887
|
// NULL SEMANTICS (null_semantics.md): a null-admitting scalar carries the tri-state slot.
|
|
872
888
|
const nmeta = nullMetaOf(f);
|
|
873
889
|
const spec: AggFieldSpec = admitsNull(f)
|
|
874
|
-
? { ...
|
|
875
|
-
:
|
|
890
|
+
? { ...surfacedSpec, nullAdmits: true, ...(nmeta.nullReason !== undefined ? { nullReason: nmeta.nullReason } : {}) }
|
|
891
|
+
: surfacedSpec;
|
|
876
892
|
if (used.has(spec.name)) {
|
|
877
893
|
throw new Error(
|
|
878
894
|
`aggregate '${agg.id}' field '${spec.wireName}' maps to duplicate Dart field '${spec.name}'`,
|
|
@@ -901,11 +917,65 @@ export function cap(s: string): string {
|
|
|
901
917
|
// `TrackableThing → TrackableThingId`, `BuildingAggregate → BuildingId`. The
|
|
902
918
|
// `RootAggregate` strip BEFORE `Aggregate` keeps the proven `SiteId` byte-identical.
|
|
903
919
|
|
|
920
|
+
// ---- Dart product-name registry (`.dartName`, architecture/dart_naming.md) ----
|
|
921
|
+
// A wire id → declared Dart base name map. When an aggregate declared `.dartName("Site")`,
|
|
922
|
+
// EVERY Dart naming derivation (concrete class, `<stem>ReadModel`, `<stem>Id`,
|
|
923
|
+
// `read/watch<Name>ById`, `ForeignRef<Target>`) consults this instead of stemming the wire id.
|
|
924
|
+
// Populated per-domain by `registerDartNames` at each Dart-emit entry point. NON-law, Dart-only
|
|
925
|
+
// (TS codegen never reads it); empty ⇒ every derivation is byte-identical to before this existed.
|
|
926
|
+
const dartNameByWireId = new Map<string, string>();
|
|
927
|
+
|
|
928
|
+
/** Register (or reset) the `.dartName` overrides for a set of aggregates before Dart emit. An
|
|
929
|
+
* aggregate WITHOUT a `.dartName` deletes any stale entry for its wire id (test hygiene across
|
|
930
|
+
* compiles in one process), so a name only ever reflects the aggregate's OWN current declaration. */
|
|
931
|
+
export function registerDartNames(aggregates: readonly AggregateHandle[]): void {
|
|
932
|
+
for (const a of aggregates) {
|
|
933
|
+
if (a.dartClassName !== undefined) dartNameByWireId.set(a.id, a.dartClassName);
|
|
934
|
+
else dartNameByWireId.delete(a.id);
|
|
935
|
+
}
|
|
936
|
+
// COLLISION GUARD (dart_naming.md): a `.dartName` must not collide with another aggregate's declared
|
|
937
|
+
// dartName OR its default id-derived concrete class — that would emit two Dart classes of the same
|
|
938
|
+
// name (a fail-closed compile error, not a silent last-wins). Checked over the concrete-class tier
|
|
939
|
+
// (the aggregate-state name); the ReadModel/Id tiers derive from it, so one check covers all tiers.
|
|
940
|
+
const byClass = new Map<string, string>(); // concrete class → wire id that produced it
|
|
941
|
+
for (const a of aggregates) {
|
|
942
|
+
const cls = dartNameByWireId.get(a.id) ?? a.id;
|
|
943
|
+
const prior = byClass.get(cls);
|
|
944
|
+
if (prior !== undefined && prior !== a.id) {
|
|
945
|
+
const how = (id: string) => (dartNameByWireId.get(id) !== undefined ? `.dartName("${cls}")` : `its wire id`);
|
|
946
|
+
throw new Error(
|
|
947
|
+
`dart naming collision: aggregate '${a.id}' (${how(a.id)}) and aggregate '${prior}' (${how(prior)}) ` +
|
|
948
|
+
`both generate the Dart class name '${cls}' — pick a distinct .dartName() for one of them`,
|
|
949
|
+
);
|
|
950
|
+
}
|
|
951
|
+
byClass.set(cls, a.id);
|
|
952
|
+
}
|
|
953
|
+
}
|
|
954
|
+
|
|
955
|
+
/** The concrete aggregate class name (aggregate STATE tier): the declared `.dartName` or the wire id
|
|
956
|
+
* (already PascalCase by convention). */
|
|
957
|
+
export function concreteClassName(aggregateType: string): string {
|
|
958
|
+
return dartNameByWireId.get(aggregateType) ?? aggregateType;
|
|
959
|
+
}
|
|
960
|
+
|
|
961
|
+
/** The Dart naming STEM for an aggregate wire type: the declared `.dartName` (used verbatim), else the
|
|
962
|
+
* wire id with a trailing `RootAggregate`/`Aggregate` stripped (the historical id-derived stem). */
|
|
963
|
+
function dartNameStem(aggregateType: string): string {
|
|
964
|
+
const declared = dartNameByWireId.get(aggregateType);
|
|
965
|
+
if (declared !== undefined) return declared;
|
|
966
|
+
let stem = aggregateType;
|
|
967
|
+
if (stem.endsWith("RootAggregate")) stem = stem.slice(0, -"RootAggregate".length);
|
|
968
|
+
else if (stem.endsWith("Aggregate")) stem = stem.slice(0, -"Aggregate".length);
|
|
969
|
+
return cap(stem === "" ? aggregateType : stem);
|
|
970
|
+
}
|
|
971
|
+
|
|
904
972
|
/** The typed-id class name for an aggregate WIRE TYPE (e.g. `SiteRootAggregate` →
|
|
905
973
|
* `SiteId`). Pure + deterministic: the SAME wire type ALWAYS yields the SAME name,
|
|
906
974
|
* so the id-class decl, the `createX` factory return, and every typed ref field that
|
|
907
|
-
* points at this type all agree. */
|
|
908
|
-
function typedIdClassForAggregateType(aggregateType: string): string {
|
|
975
|
+
* points at this type all agree. Honours `.dartName` (`Site` → `SiteId`). */
|
|
976
|
+
export function typedIdClassForAggregateType(aggregateType: string): string {
|
|
977
|
+
const declared = dartNameByWireId.get(aggregateType);
|
|
978
|
+
if (declared !== undefined) return `${declared}Id`;
|
|
909
979
|
let stem = aggregateType;
|
|
910
980
|
if (stem.endsWith("RootAggregate")) {
|
|
911
981
|
stem = stem.slice(0, -"RootAggregate".length);
|
|
@@ -1171,7 +1241,7 @@ function emitAggregate(
|
|
|
1171
1241
|
refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
|
|
1172
1242
|
ownIdFields: ReadonlySet<string> = new Set(),
|
|
1173
1243
|
): string {
|
|
1174
|
-
const className = agg.id; // wire id
|
|
1244
|
+
const className = concreteClassName(agg.id); // wire id (already PascalCase) unless `.dartName` overrides.
|
|
1175
1245
|
const idType = aggregateIdDartType(agg.id);
|
|
1176
1246
|
const specs = aggregateFieldSpecs(
|
|
1177
1247
|
agg,
|
|
@@ -1203,6 +1273,14 @@ function emitAggregate(
|
|
|
1203
1273
|
` final NomosRef ref;`,
|
|
1204
1274
|
...specs.flatMap((s) => {
|
|
1205
1275
|
const doc = s.nullReason !== undefined ? [` /// null semantic: ${s.nullReason}`] : [];
|
|
1276
|
+
// SURFACED MERGE CONFLICTS: the field reads as the plain scalar, or [SurfacedPending]
|
|
1277
|
+
// while genuinely concurrent writes await a human pick (resolve = an ordinary next write).
|
|
1278
|
+
if (s.surfaced === true) {
|
|
1279
|
+
return [
|
|
1280
|
+
` /// Surfaced merge conflicts: [SurfacedResolved] steady state, [SurfacedPending] under conflict.`,
|
|
1281
|
+
` final SurfacedValue<${s.dartType}>? ${s.name};`,
|
|
1282
|
+
];
|
|
1283
|
+
}
|
|
1206
1284
|
// NULL SEMANTICS: a null-admitting field is the sealed tri-state slot (Omitted / ExplicitNull /
|
|
1207
1285
|
// Present) — omission and explicit-null never collapse. Others keep the plain `T?`.
|
|
1208
1286
|
return s.nullAdmits === true
|
|
@@ -1236,9 +1314,11 @@ function emitAggregate(
|
|
|
1236
1314
|
` aggregateId: ${aggregateIdFromRowExpr(agg.id, "id")},`,
|
|
1237
1315
|
` ref: NomosRef(workspace: workspace, type: type.isEmpty ? '${agg.id}' : type, id: id),`,
|
|
1238
1316
|
...specs.map((s) =>
|
|
1239
|
-
s.
|
|
1240
|
-
? ` ${s.name}:
|
|
1241
|
-
:
|
|
1317
|
+
s.surfaced === true
|
|
1318
|
+
? ` ${s.name}: SurfacedValue.decode<${s.dartType}>(data, '${s.wireName}', ${slotDecodeCallback(s, "aggregate")}),`
|
|
1319
|
+
: s.nullAdmits === true
|
|
1320
|
+
? ` ${s.name}: NomosSlot.decode<${stripNullable(s.dartType)}>(data, '${s.wireName}', ${slotDecodeCallback(s, "aggregate")}),`
|
|
1321
|
+
: ` ${s.name}: ${parseField(s, className)},`,
|
|
1242
1322
|
),
|
|
1243
1323
|
].join("\n");
|
|
1244
1324
|
|
|
@@ -1797,7 +1877,7 @@ function emitAggregateMetaRegistry(
|
|
|
1797
1877
|
` fields: {`,
|
|
1798
1878
|
...fieldLines,
|
|
1799
1879
|
` },`,
|
|
1800
|
-
` fromRow: ${agg.id}.fromJson,`,
|
|
1880
|
+
` fromRow: ${concreteClassName(agg.id)}.fromJson,`,
|
|
1801
1881
|
` ),`,
|
|
1802
1882
|
].join("\n");
|
|
1803
1883
|
});
|
|
@@ -2367,13 +2447,7 @@ function stripNullable(t: string): string {
|
|
|
2367
2447
|
* `BuildingReadModel`, `ThingReadModel`, `TrackableThingReadModel`). Pure + deterministic
|
|
2368
2448
|
* — the same wire type always yields the same name. */
|
|
2369
2449
|
export function readModelClassName(aggregateType: string): string {
|
|
2370
|
-
|
|
2371
|
-
if (stem.endsWith("RootAggregate")) {
|
|
2372
|
-
stem = stem.slice(0, -"RootAggregate".length);
|
|
2373
|
-
} else if (stem.endsWith("Aggregate")) {
|
|
2374
|
-
stem = stem.slice(0, -"Aggregate".length);
|
|
2375
|
-
}
|
|
2376
|
-
return `${cap(stem === "" ? aggregateType : stem)}ReadModel`;
|
|
2450
|
+
return `${dartNameStem(aggregateType)}ReadModel`;
|
|
2377
2451
|
}
|
|
2378
2452
|
|
|
2379
2453
|
/** The Dart type for one generated read-model field. */
|
|
@@ -2560,6 +2634,13 @@ function emitReadModel(
|
|
|
2560
2634
|
` final NomosRef ref;`,
|
|
2561
2635
|
...specs.flatMap((s) => {
|
|
2562
2636
|
const doc = s.nullReason !== undefined ? [` /// null semantic: ${s.nullReason}`] : [];
|
|
2637
|
+
// SURFACED MERGE CONFLICTS: pending observations surface typed; resolve = ordinary next write.
|
|
2638
|
+
if (s.surfaced === true) {
|
|
2639
|
+
return [
|
|
2640
|
+
` /// Surfaced merge conflicts: [SurfacedResolved] steady state, [SurfacedPending] under conflict.`,
|
|
2641
|
+
` final SurfacedValue<${readModelDartType(s)}>? ${s.name};`,
|
|
2642
|
+
];
|
|
2643
|
+
}
|
|
2563
2644
|
return s.nullAdmits === true
|
|
2564
2645
|
? [...doc, ` final NomosSlot<${stripNullable(readModelDartType(s))}> ${s.name};`]
|
|
2565
2646
|
: [` final ${readModelDartType(s)}${readModelNullableSuffix(s)} ${s.name};`];
|
|
@@ -2591,13 +2672,39 @@ function emitReadModel(
|
|
|
2591
2672
|
...combineds.map((c) => ` this.${c.id},`),
|
|
2592
2673
|
].join("\n");
|
|
2593
2674
|
|
|
2675
|
+
// SURFACED MERGE CONFLICTS: per-Surfaced-field convenience accessors (the pending seam an app
|
|
2676
|
+
// branches on without hand-parsing) — an EXTENSION on the read model, mirroring the ref-traversal
|
|
2677
|
+
// accessor pattern (codegen_dart_client.ts P2). They live HERE (the pure types file), not on the
|
|
2678
|
+
// client: they are pure field reads needing no bridge, and in layered-packages mode the types
|
|
2679
|
+
// package must not depend on the client. A Surfaced-free aggregate emits none (byte-identical).
|
|
2680
|
+
const surfacedSpecs = specs.filter((s) => s.surfaced === true);
|
|
2681
|
+
const surfacedExtension =
|
|
2682
|
+
surfacedSpecs.length === 0
|
|
2683
|
+
? []
|
|
2684
|
+
: [
|
|
2685
|
+
``,
|
|
2686
|
+
`/// Pending-conflict accessors for [${className}] (surfaced_merge_conflict.md) — branch on`,
|
|
2687
|
+
`/// pending-ness without hand-parsing; the UI decides what to do (resolution is an ordinary`,
|
|
2688
|
+
`/// next write via the domain's own directive).`,
|
|
2689
|
+
`extension ${className}Surfaced on ${className} {`,
|
|
2690
|
+
...surfacedSpecs.flatMap((s) => [
|
|
2691
|
+
` /// True while \`${s.wireName}\` awaits a human resolution (competing observations retained).`,
|
|
2692
|
+
` bool get has${cap(s.name)}PendingConflict => ${s.name}?.isPending ?? false;`,
|
|
2693
|
+
` /// The competing observations on \`${s.wireName}\` while pending; empty otherwise.`,
|
|
2694
|
+
` List<SurfacedObservation<${readModelDartType(s)}>> get ${s.name}PendingObservations => ${s.name}?.pending ?? const [];`,
|
|
2695
|
+
]),
|
|
2696
|
+
`}`,
|
|
2697
|
+
];
|
|
2698
|
+
|
|
2594
2699
|
const parseLines = [
|
|
2595
2700
|
` aggregateId: ${aggregateIdFromRowExpr(agg.id, "id")},`,
|
|
2596
2701
|
` ref: NomosRef(workspace: workspace, type: type.isEmpty ? '${agg.id}' : type, id: id),`,
|
|
2597
2702
|
...specs.map((s) =>
|
|
2598
|
-
s.
|
|
2599
|
-
? ` ${s.name}:
|
|
2600
|
-
:
|
|
2703
|
+
s.surfaced === true
|
|
2704
|
+
? ` ${s.name}: SurfacedValue.decode<${readModelDartType(s)}>(data, '${s.wireName}', ${slotDecodeCallback(s, "readModel")}),`
|
|
2705
|
+
: s.nullAdmits === true
|
|
2706
|
+
? ` ${s.name}: NomosSlot.decode<${stripNullable(readModelDartType(s))}>(data, '${s.wireName}', ${slotDecodeCallback(s, "readModel")}),`
|
|
2707
|
+
: ` ${s.name}: ${parseReadModelField(s, wireType)},`,
|
|
2601
2708
|
),
|
|
2602
2709
|
// Derived/combined fields decode from the engine-produced bare JSON leaf.
|
|
2603
2710
|
...deriveds.map((d) => ` ${d.id}: ${parseProjectionReturnField(d.id, d.returns)},`),
|
|
@@ -2636,6 +2743,7 @@ function emitReadModel(
|
|
|
2636
2743
|
` );`,
|
|
2637
2744
|
` }`,
|
|
2638
2745
|
`}`,
|
|
2746
|
+
...surfacedExtension,
|
|
2639
2747
|
].join("\n");
|
|
2640
2748
|
}
|
|
2641
2749
|
|
|
@@ -2968,7 +3076,8 @@ function emitQueryReads(
|
|
|
2968
3076
|
refIdClassForAggregate: (aggregateType: string) => string = typedIdClassForAggregateType,
|
|
2969
3077
|
ownIdFieldsByAggregate: ReadonlyMap<string, ReadonlySet<string>> = new Map(),
|
|
2970
3078
|
): string {
|
|
2971
|
-
const agg = q.returns; // the aggregate TYPE id (== the
|
|
3079
|
+
const agg = q.returns; // the aggregate TYPE id (== the wire id, used for field/index lookups).
|
|
3080
|
+
const aggClass = concreteClassName(agg); // the generated Dart class name (honours `.dartName`).
|
|
2972
3081
|
const method = readMethodName(q.id);
|
|
2973
3082
|
const Method = cap(method);
|
|
2974
3083
|
// The query registry keys every query on EXACTLY ONE string field; take it as
|
|
@@ -2995,17 +3104,17 @@ function emitQueryReads(
|
|
|
2995
3104
|
const keyParam = `${keySpec.dartType} ${paramName}`;
|
|
2996
3105
|
const words = q.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
2997
3106
|
return [
|
|
2998
|
-
`/// Reactive typed read: every \`${
|
|
3107
|
+
`/// Reactive typed read: every \`${aggClass}\` matching the indexed \`${q.id}\` query`,
|
|
2999
3108
|
`/// (${words}), keyed on \`${keyField}\`. Emits the current matches, then re-emits`,
|
|
3000
3109
|
`/// on each projection tick. O(matches) via the projection index (#140) — never a`,
|
|
3001
3110
|
`/// workspace scan.`,
|
|
3002
|
-
`Stream<List<${
|
|
3003
|
-
` reads.watchQuery('${q.id}', {${params}}, ${
|
|
3111
|
+
`Stream<List<${aggClass}>> watch${Method}(NomosReads reads, ${keyParam}) =>`,
|
|
3112
|
+
` reads.watchQuery('${q.id}', {${params}}, ${aggClass}.fromJson);`,
|
|
3004
3113
|
``,
|
|
3005
3114
|
`/// One-shot typed read of the \`${q.id}\` query (${words}): the CURRENT matching`,
|
|
3006
|
-
`/// \`${
|
|
3007
|
-
`List<${
|
|
3008
|
-
` reads.readQuery('${q.id}', {${params}}, ${
|
|
3115
|
+
`/// \`${aggClass}\` list, keyed on \`${keyField}\`. O(matches).`,
|
|
3116
|
+
`List<${aggClass}> read${Method}(NomosReads reads, ${keyParam}) =>`,
|
|
3117
|
+
` reads.readQuery('${q.id}', {${params}}, ${aggClass}.fromJson);`,
|
|
3009
3118
|
].join("\n");
|
|
3010
3119
|
}
|
|
3011
3120
|
|
|
@@ -3014,7 +3123,7 @@ function emitQueryReads(
|
|
|
3014
3123
|
* off the PK `query_by_id` seam (resolves the unbound singleton by its type-key AND
|
|
3015
3124
|
* an instance-bound aggregate by its id). */
|
|
3016
3125
|
function emitAggregateByIdReads(agg: AggregateHandle): string {
|
|
3017
|
-
const cls = agg.id;
|
|
3126
|
+
const cls = concreteClassName(agg.id);
|
|
3018
3127
|
const idType = aggregateIdDartType(agg.id);
|
|
3019
3128
|
return [
|
|
3020
3129
|
`/// Reactive typed by-id read: the \`${cls}\` with kernel aggregate id [id]`,
|
|
@@ -3318,7 +3427,7 @@ function emitExistsReads(
|
|
|
3318
3427
|
* Returns projected typed aggregates (via `project_by_ids`, `lib.rs:1686`) — not raw ids.
|
|
3319
3428
|
* Archived aggregates are auto-excluded (the `type_index` row is dropped on archive). */
|
|
3320
3429
|
function emitOrderedReads(d: OrderedReadDecl): string {
|
|
3321
|
-
const agg = d.of;
|
|
3430
|
+
const agg = concreteClassName(d.of); // the generated Dart class name (honours `.dartName`).
|
|
3322
3431
|
const method = readMethodName(d.id);
|
|
3323
3432
|
const Method = cap(method);
|
|
3324
3433
|
const words = d.id.replace(/([a-z0-9])([A-Z])/g, "$1 $2").toLowerCase();
|
|
@@ -3708,6 +3817,14 @@ export interface DomainModule {
|
|
|
3708
3817
|
* (TS + Dart). NOT law, NOT hashed (like {@link autoDeclaredEnsures}). Absent when no role is declared.
|
|
3709
3818
|
*/
|
|
3710
3819
|
roles?: import("./role.js").RoleMeta[];
|
|
3820
|
+
/**
|
|
3821
|
+
* THE DESIGNATED SURFACED RESOLVERS (surfaced_lint.ts) — one binding per `.merge(Surfaced(...))`
|
|
3822
|
+
* field, produced (and fail-closed enforced: exactly one `.resolves()` per Surfaced field, every
|
|
3823
|
+
* Surfaced field covered) by `composeDomainModule`. Drives the generated typed `resolve…` helpers
|
|
3824
|
+
* (TS + Dart) and the `pendingConflicts` accessor. NOT law, NOT hashed (like {@link roles}).
|
|
3825
|
+
* Absent when the domain declares no Surfaced field (byte-identical — the era rule).
|
|
3826
|
+
*/
|
|
3827
|
+
surfacedResolvers?: import("./surfaced_lint.js").SurfacedResolverBinding[];
|
|
3711
3828
|
/**
|
|
3712
3829
|
* STABLE-ID CONTINUITY (#58 — names are labels, identity is minted). When present
|
|
3713
3830
|
* (attached by `nomos-compile` from the PRIOR build's identity manifests), the
|
|
@@ -3977,6 +4094,7 @@ export interface GenerateDartDomainOptions {
|
|
|
3977
4094
|
|
|
3978
4095
|
/** Generate the full Dart source for one domain module. */
|
|
3979
4096
|
export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOptions = {}): string {
|
|
4097
|
+
registerDartNames(mod.aggregates); // `.dartName` overrides for this domain's Dart naming derivations.
|
|
3980
4098
|
const domainKey = mod.domain ?? mod.name;
|
|
3981
4099
|
const skipAggregateIds = opts.skipAggregateIds ?? new Set<string>();
|
|
3982
4100
|
const skipDirectiveIds = opts.skipDirectiveIds ?? new Set<string>();
|
|
@@ -4075,7 +4193,13 @@ export function generateDartDomain(mod: DomainModule, opts: GenerateDartDomainOp
|
|
|
4075
4193
|
// `jsonEncode` — the ONE case the generated file needs `dart:convert` (null_semantics.md).
|
|
4076
4194
|
const needsJsonConvert = localAggregates.some((a) =>
|
|
4077
4195
|
Object.values(a.fields as Record<string, Field>).some(
|
|
4078
|
-
(f) =>
|
|
4196
|
+
(f) =>
|
|
4197
|
+
// A null-admitting plain-json slot re-stringifies via jsonEncode…
|
|
4198
|
+
(admitsNull(f) && f.kind === "json" && f.valueObject === undefined) ||
|
|
4199
|
+
// …and so does a Surfaced plain-json field's aggregate-class leaf decode.
|
|
4200
|
+
((f.driver as { kind?: string } | undefined)?.kind === "Surfaced" &&
|
|
4201
|
+
f.kind === "json" &&
|
|
4202
|
+
f.valueObject === undefined),
|
|
4079
4203
|
),
|
|
4080
4204
|
);
|
|
4081
4205
|
const localDirectives = mod.directives.filter((d) => !skipDirectiveIds.has(d.id));
|