@githolon/dsl 0.80.1 → 0.80.2

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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@githolon/dsl",
3
- "version": "0.80.1",
3
+ "version": "0.80.2",
4
4
  "type": "module",
5
5
  "description": "Nomos 2 domain-authoring DSL: aggregates + directives in TS, executed and encoded to the Rust kernel's wire shapes.",
6
6
  "license": "SEE LICENSE IN LICENSE.md",
@@ -124,6 +124,7 @@ import {
124
124
  } from "./stable_ids.js";
125
125
  import { emitUsd, type UsdLayer } from "./usd.js";
126
126
  import { emitLayeredRelationUsda, type RelationLayerInput } from "./relation_composition.js";
127
+ import { assertNoDartBarrelCollisions, type BarrelExportedFile } from "./dart_barrel_guard.js";
127
128
  import { generateTsClient, tsClientFactoryName, recursiveLazyTypes } from "./codegen_ts.js";
128
129
  import { generateOpenApiFragment, mergeOpenApiFragments } from "./codegen_openapi.js";
129
130
  import { domainInterface, type DomainInterface } from "./shape_compat.js";
@@ -892,6 +893,17 @@ function emitDart(
892
893
  // dedup above makes the exports collision-free by construction; re-exporting the same library
893
894
  // through two paths — e.g. a client's own `nomos_framework.dart` re-export — is not ambiguous).
894
895
  const genRel = DART_GENERATED_SUBDIR.split(path.sep).join("/");
896
+ // STRUCTURAL GUARD (dart_barrel_guard.ts): the barrel flat-exports every generated TYPES file, so no
897
+ // public name may be declared in ≥2 of them — else `dart analyze` fails the package with
898
+ // ambiguous_export. Refuse the compile NOW (before we write an uncompilable surface) with a named,
899
+ // actionable error. The framework-shared dedup keeps this green; this catches any future gap in it.
900
+ try {
901
+ assertNoDartBarrelCollisions(
902
+ [...sourceByFile.entries()].map(([label, src]): BarrelExportedFile => ({ label, src })),
903
+ );
904
+ } catch (e) {
905
+ fail((e as Error).message);
906
+ }
895
907
  const barrelLines = [
896
908
  "// GENERATED by nomos-compile — the package's public entrypoint. DO NOT EDIT.",
897
909
  "// Regenerated on every compile; generated implementation lives under src/generated/.",
@@ -1196,6 +1208,16 @@ function emitDartPackages(
1196
1208
  `// Import a domain with a prefix instead, e.g. \`import '${genRel}/<domain>.dart' as <domain>;\`.`,
1197
1209
  ]
1198
1210
  : [];
1211
+ // STRUCTURAL GUARD (dart_barrel_guard.ts): refuse a types barrel that would flat-export the SAME public
1212
+ // name from ≥2 files (ambiguous_export). Only the files the barrel ACTUALLY flat-exports are checked —
1213
+ // under `barrelExports: 'explicit'` per-domain files are withheld, so they cannot collide there.
1214
+ try {
1215
+ assertNoDartBarrelCollisions(
1216
+ typesExportKeys.map((f): BarrelExportedFile => ({ label: f, src: typesSources.get(f) ?? "" })),
1217
+ );
1218
+ } catch (e) {
1219
+ fail((e as Error).message);
1220
+ }
1199
1221
  const typesBarrel = [
1200
1222
  "// GENERATED by nomos-compile — the TYPES package's public entrypoint. DO NOT EDIT.",
1201
1223
  "// Pure data: payload classes, read models, VOs/enums, typed read accessors over the",
@@ -0,0 +1,78 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted
2
+ // remotes are replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon wasm.
3
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
4
+
5
+ /**
6
+ * DART BARREL COLLISION GUARD — compile self-refuses an uncompilable Dart surface.
7
+ *
8
+ * A Nomos package's generated Dart is exposed through ONE package barrel that FLAT-EXPORTS each
9
+ * generated file (`export 'src/generated/estate.dart';`, …). Dart's `export` brings every PUBLIC
10
+ * top-level declaration into the barrel's namespace; if the SAME public name is declared in ≥2
11
+ * flat-exported files, `dart analyze` fails the whole package with `ambiguous_export` — an error the
12
+ * tenant cannot fix without hand-editing generated code or restructuring their package.
13
+ *
14
+ * The framework-shared dedup (`nomos_framework.dart`) is meant to make this impossible by
15
+ * construction (framework symbols emit ONCE), but any gap in that dedup — a framework payload class
16
+ * NOT routed to the shared file, a VO emitted per-domain — reintroduces the collision silently. This
17
+ * guard is the STRUCTURAL backstop: we KNOW the public names we emit and which files the barrel
18
+ * flat-exports, so we detect the ambiguity at COMPILE time (no `dart analyze` needed) and REFUSE with
19
+ * a named, actionable error. Third occurrence of this bug class in layered Dart — this closes it for good.
20
+ */
21
+
22
+ /** One flat-exported generated file the barrel re-exports: a human label (the domain / file name) + its Dart source. */
23
+ export interface BarrelExportedFile {
24
+ /** The file's identifying label used in the error message (e.g. the domain key `estate`). */
25
+ readonly label: string;
26
+ /** The generated Dart source. */
27
+ readonly src: string;
28
+ }
29
+
30
+ /** Declared PUBLIC top-level type/typedef names in a Dart source (class/enum/mixin/extension type/typedef).
31
+ * Private (`_`-prefixed) names never leak through an `export`, so they cannot collide — excluded. */
32
+ export function publicDartDeclarations(src: string): Set<string> {
33
+ const names = new Set<string>();
34
+ const re =
35
+ /^(?:abstract\s+|base\s+|final\s+|sealed\s+|interface\s+|mixin\s+)*(?:class|enum|mixin|extension type|typedef)\s+([A-Za-z][A-Za-z0-9_]*)/gm;
36
+ for (let m = re.exec(src); m !== null; m = re.exec(src)) {
37
+ if (!m[1]!.startsWith("_")) names.add(m[1]!);
38
+ }
39
+ return names;
40
+ }
41
+
42
+ /** name → the sorted labels of the flat-exported files that each DECLARE it. Only entries with ≥2 files
43
+ * are collisions (an `ambiguous_export`); a name declared in exactly one file is fine. */
44
+ export function findDartBarrelCollisions(
45
+ files: readonly BarrelExportedFile[],
46
+ ): Map<string, string[]> {
47
+ const byName = new Map<string, Set<string>>();
48
+ for (const f of files) {
49
+ for (const name of publicDartDeclarations(f.src)) {
50
+ const set = byName.get(name) ?? new Set<string>();
51
+ set.add(f.label);
52
+ byName.set(name, set);
53
+ }
54
+ }
55
+ const collisions = new Map<string, string[]>();
56
+ for (const [name, labels] of byName) {
57
+ if (labels.size >= 2) collisions.set(name, [...labels].sort());
58
+ }
59
+ return collisions;
60
+ }
61
+
62
+ /** THROW a named, actionable error if the flat-exported files declare any public name more than once.
63
+ * Green (no-op) when the surface is collision-free — the state every valid package MUST be in. */
64
+ export function assertNoDartBarrelCollisions(files: readonly BarrelExportedFile[]): void {
65
+ const collisions = findDartBarrelCollisions(files);
66
+ if (collisions.size === 0) return;
67
+ const lines = [...collisions.entries()]
68
+ .sort(([a], [b]) => (a < b ? -1 : 1))
69
+ .map(([name, labels]) => ` • '${name}' is declared in ${labels.map((l) => `'${l}'`).join(", ")}`);
70
+ throw new Error(
71
+ `generated Dart would not compile — the package barrel flat-exports the SAME public name from ` +
72
+ `multiple files, so 'dart analyze' fails with ambiguous_export:\n${lines.join("\n")}\n` +
73
+ `A public name emitted per-domain must be FRAMEWORK-SHARED (emitted ONCE in nomos_framework.dart) ` +
74
+ `— this is a compiler bug, not a tenant one; do not hand-edit generated code. If a FRAMEWORK ` +
75
+ `directive/aggregate/value-object payload is colliding, it must carry the framework brand so the ` +
76
+ `dedup routes it to the shared file (see framework_aggregates.ts / secondary_ensure.ts).`,
77
+ );
78
+ }
@@ -53,10 +53,22 @@ export function sanctioningEnsureDirectiveId(aggId: string): string {
53
53
  * lane for a plan's secondary deterministic `ensure(agg, …)`.
54
54
  */
55
55
  export function sanctioningEnsureDirective(agg: AggregateHandle): Directive<unknown> {
56
- return directive(sanctioningEnsureDirectiveId(agg.id))
56
+ const dir = directive(sanctioningEnsureDirectiveId(agg.id))
57
57
  .ensures(agg)
58
58
  .payload(z.object({}).strict())
59
59
  .plan(() => []) as unknown as Directive<unknown>;
60
+ // FRAMEWORK-SHARED DEDUP (Dart codegen): when the sanctioned aggregate is a FRAMEWORK singleton
61
+ // (e.g. RelationTuple — auto-ensured into EVERY relation-using domain), this directive is injected
62
+ // per-domain but its payload class (`NomosEnsure_<Agg>Payload`, zero-field) is byte-identical across
63
+ // all of them. Left unbranded, each domain's Dart file re-defines the class and a layered types
64
+ // barrel flat-exporting ≥2 domains collides (`ambiguous_export`). Brand it framework — exactly the
65
+ // `nomosSourceFrontierEnsure` precedent — so its payload dedups into `nomos_framework.dart` ONCE.
66
+ // A sanction over a TENANT aggregate stays per-domain (its aggregate lives in that one domain, so no
67
+ // cross-domain collision is possible) — hence gating on the framework brand, not blanket branding.
68
+ if ((agg as { framework?: true }).framework === true) {
69
+ (dir as { framework?: true }).framework = true;
70
+ }
71
+ return dir;
60
72
  }
61
73
 
62
74
  /**