@githolon/dsl 0.16.3 → 0.18.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/src/codegen_ts.ts CHANGED
@@ -534,7 +534,7 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
534
534
  // the obligation the birth provider materializes. parentWs = "root" for a top-level platform,
535
535
  // else the parent holon's own name. timestamps ride IN the payload (the determinism doctrine);
536
536
  // a platform's foundingAdminHash = sha256(your chosen platformAdminKey) — the host never mints it.
537
- const emit = (name: string, doc: string, params: string[], lane: { kind: "boot" } | { kind: "offer"; parentWs: string; directiveId: "createWorkspace" | "birthPlatform" }) => {
537
+ const emit = (name: string, doc: string, params: string[], lane: { kind: "boot" } | { kind: "offer"; parentWs: string; directiveId: "createWorkspace" | "birthChild" }) => {
538
538
  if (emittedBirths.has(name)) return;
539
539
  emittedBirths.add(name);
540
540
  const sig = `opts: NomosBirthAuth & { cloud: string; ${params.join("; ")} }`;
@@ -547,14 +547,18 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
547
547
  );
548
548
  return;
549
549
  }
550
- const isPlatformOffer = lane.directiveId === "birthPlatform";
550
+ // A platform birth is birthChild WITH genesis (the tier collapsed onto #creator; birthPlatform is
551
+ // gone). The genesis stamps ride IN the payload (the determinism doctrine): foundingAdminHash =
552
+ // sha256(your chosen platformAdminKey), bornAt = the ISO birth time. The host replays the
553
+ // law-authored chain — it mints no genesis.
554
+ const isPlatformOffer = lane.directiveId === "birthChild";
551
555
  const bodyFields = [
552
556
  `name: opts.name`,
553
557
  `parent: ${lane.parentWs}`,
554
558
  `principal: opts.principal ?? ""`,
555
559
  `requestedBy: opts.principal ?? ""`,
556
560
  `requestedAt: Date.now()`,
557
- ...(isPlatformOffer ? [`foundingAdminHash: await nomosKeyHash(opts.platformAdminKey)`] : []),
561
+ ...(isPlatformOffer ? [`foundingAdminHash: await nomosKeyHash(opts.platformAdminKey)`, `bornAt: new Date().toISOString()`] : []),
558
562
  ];
559
563
  out.push(
560
564
  `/** ${doc} */`,
@@ -570,13 +574,13 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
570
574
  const isPlatform = t.childIds.length > 0;
571
575
  if (!childIds.has(t.id)) {
572
576
  // A TOP-LEVEL type: a workspace births on the boot lane; a platform-kind
573
- // coordinator births via the root holon's birthPlatform offer.
577
+ // coordinator births via the root holon's birthChild offer (with genesis).
574
578
  emit(
575
579
  fnName(t.id),
576
- `Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree; PRESENT your chosen platformAdminKey) via root's birthPlatform offer" : " on the boot lane (the one-time secret comes back)"} — derived from the taxonomy. Placement is automatic (measured shape) — no tier to pick.`,
580
+ `Birth a '${t.id}' workspace${isPlatform ? " (a platform-kind coordinator: hosts its own subtree; PRESENT your chosen platformAdminKey) via root's birthChild offer" : " on the boot lane (the one-time secret comes back)"} — derived from the taxonomy. Placement is automatic (measured shape) — no tier to pick.`,
577
581
  isPlatform ? [`name: string`, `platformAdminKey: string`] : [`name: string`],
578
582
  isPlatform
579
- ? { kind: "offer", parentWs: `"root"`, directiveId: "birthPlatform" }
583
+ ? { kind: "offer", parentWs: `"root"`, directiveId: "birthChild" }
580
584
  : { kind: "boot" },
581
585
  );
582
586
  }
@@ -587,11 +591,11 @@ export function generateTsClient(modules: readonly DomainModule[], opts: TsClien
587
591
  const childIsPlatform = child.childIds.length > 0;
588
592
  emit(
589
593
  fnName(child.id),
590
- `Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge, a governance offer on the parent holon (POST /v2/workspaces/<${parentParam}>/${childIsPlatform ? "birthPlatform" : "createWorkspace"}; lineage + pool enforced by the parent's law).${childIsPlatform ? " PRESENT your chosen platformAdminKey." : ""} Placement is automatic (measured shape).`,
594
+ `Birth a '${child.id}' under a '${t.id}' — the taxonomy's hasMany edge, a governance offer on the parent holon (POST /v2/workspaces/<${parentParam}>/${childIsPlatform ? "birthChild" : "createWorkspace"}; lineage + pool enforced by the parent's law).${childIsPlatform ? " PRESENT your chosen platformAdminKey." : ""} Placement is automatic (measured shape).`,
591
595
  childIsPlatform
592
596
  ? [`${parentParam}: string`, `name: string`, `platformAdminKey: string`]
593
597
  : [`${parentParam}: string`, `name: string`],
594
- { kind: "offer", parentWs: `opts.${parentParam}`, directiveId: childIsPlatform ? "birthPlatform" : "createWorkspace" },
598
+ { kind: "offer", parentWs: `opts.${parentParam}`, directiveId: childIsPlatform ? "birthChild" : "createWorkspace" },
595
599
  );
596
600
  }
597
601
  }
@@ -61,30 +61,37 @@ import {
61
61
  type PermissionVocabulary,
62
62
  } from "./codegen_dart.js";
63
63
  import { generateDartBridgeClient } from "./codegen_dart_client.js";
64
- import type { AnyCount } from "./count.js";
64
+ import { finishCount, type AnyCount } from "./count.js";
65
65
  import type { QueryDecl } from "./query.js";
66
66
  import type { SpatialDecl } from "./spatial.js";
67
- import type { AnySum } from "./sum.js";
67
+ import { finishSum, type AnySum } from "./sum.js";
68
68
  import type { DerivedDecl } from "./derived.js";
69
69
  import type { CombinedDecl } from "./combined.js";
70
70
  import type { ImpureCapabilityDecl } from "./codegen_provider_dart.js";
71
71
  import {
72
72
  aggregatesOf,
73
73
  buildIdentity,
74
- buildReadManifest,
75
74
  composeDomainModule,
76
75
  directivesOf,
77
76
  emitUsdJsonForModules,
78
77
  packageUsda,
79
78
  sha256HexUtf8,
80
- writeIdentity,
81
79
  type Mod,
82
80
  } from "./build_package.js";
83
81
  import { collectEngineRouting } from "./engine_entry.js";
82
+ import {
83
+ discover,
84
+ isCountDecl,
85
+ isQueryDecl,
86
+ isSpatialDecl,
87
+ isSumDecl,
88
+ isWorkspaceInvariantDecl,
89
+ type DeclBag,
90
+ } from "./decl_shapes.js";
84
91
  import { resolveWorkspaceTypes, type WorkspaceTypeDecl } from "./workspace_type.js";
85
92
  import { canonicalTaxonomyFragment, validateWorkspaceTaxonomyAndRoutes } from "./workspace_routing.js";
86
93
  import { shardingLawModule, type ShardingLawSpec } from "./workspace_sharding.js";
87
- import type { AnyExtremum } from "./extremum.js";
94
+ import { finishExtremum, type AnyExtremum } from "./extremum.js";
88
95
  import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
89
96
  import { type RelationTypeDecl, isRelationTypeDecl, validateRelationType } from "./relation_schema.js";
90
97
  import {
@@ -97,7 +104,6 @@ import { domainManifest } from "./manifest.js";
97
104
  import type { WireSchema } from "./wire.js";
98
105
  import {
99
106
  deriveStableIdContinuity,
100
- stableIdsFromManifestJson,
101
107
  type InferredRename,
102
108
  type StableIds,
103
109
  } from "./stable_ids.js";
@@ -192,37 +198,9 @@ function fail(msg: string): never {
192
198
  }
193
199
 
194
200
  // ── decl auto-discovery (by SHAPE, mirroring the engine entry's duck-types) ───────
195
-
196
- function isQueryDecl(v: unknown): v is QueryDecl {
197
- return (
198
- !!v &&
199
- typeof v === "object" &&
200
- typeof (v as { id?: unknown }).id === "string" &&
201
- Array.isArray((v as { key?: unknown }).key) &&
202
- ((v as { key: unknown[] }).key as unknown[]).every((k) => typeof k === "string") &&
203
- typeof (v as { returns?: unknown }).returns === "string" &&
204
- typeof (v as { fn?: unknown }).fn !== "function"
205
- );
206
- }
207
-
208
- function isWorkspaceInvariantDecl(v: unknown): v is WorkspaceInvariantDecl {
209
- return (
210
- !!v &&
211
- typeof v === "object" &&
212
- (v as { __isWorkspaceInvariant?: unknown }).__isWorkspaceInvariant === true
213
- );
214
- }
215
-
216
- function isSpatialDecl(v: unknown): v is SpatialDecl {
217
- return (
218
- !!v &&
219
- typeof v === "object" &&
220
- typeof (v as { id?: unknown }).id === "string" &&
221
- typeof (v as { of?: unknown }).of === "string" &&
222
- typeof (v as { on?: unknown }).on === "string" &&
223
- typeof (v as { fn?: unknown }).fn !== "function"
224
- );
225
- }
201
+ // The query/workspace-invariant/spatial/sum/count predicates + `discover` live in the SHARED
202
+ // bundle-safe `decl_shapes.ts` (imported above) — the lump's self-describe verb discovers by the
203
+ // EXACT same shape so the kernel-served OpenAPI matches the compiled `<name>.openapi.json`.
226
204
 
227
205
  function isDerivedDecl(v: unknown): v is DerivedDecl {
228
206
  return (
@@ -250,35 +228,6 @@ function isCombinedDecl(v: unknown): v is CombinedDecl {
250
228
  );
251
229
  }
252
230
 
253
- /** A sum decl/builder — the `{id, of, sumField}` mark (counts/extremums never carry `sumField`). */
254
- function isSumDecl(v: unknown): v is AnySum {
255
- if (!v || typeof v !== "object") return false;
256
- const o = v as Record<string, unknown>;
257
- return typeof o.id === "string" && typeof o.of === "string" && typeof o.sumField === "string";
258
- }
259
-
260
- /** A count decl/builder — `{id, of}` with NONE of the other decl families' marks. */
261
- function isCountDecl(v: unknown): v is AnyCount {
262
- if (!v || typeof v !== "object") return false;
263
- const o = v as Record<string, unknown>;
264
- return (
265
- typeof o.id === "string" &&
266
- typeof o.of === "string" &&
267
- typeof o.on !== "string" && // spatial
268
- typeof o.fn !== "function" && // derived/combined
269
- !Array.isArray(o.key) && // query
270
- !("field" in o) && // extremum (wire-descriptor shape)
271
- !("valueField" in o) && // extremum (the ExtremumDecl/Extremum-builder mark — never a count)
272
- !("sumField" in o) && // sum (the SumDecl/Sum-builder mark — never a count)
273
- !("orderBy" in o) && // ordered read
274
- !("_existsMarker" in o) && // exists
275
- typeof o.refField !== "string" && // combined
276
- o.__isAggregateHandle !== true && // aggregate
277
- typeof o.aggregateId !== "string" && // directive
278
- typeof o.plan !== "function"
279
- );
280
- }
281
-
282
231
  /**
283
232
  * Scan merged exports for `workspaceType(...)` decls (sharding §10 slice 1) by the
284
233
  * `__isWorkspaceType` brand — individual exports AND exported arrays. Dedupe is BY
@@ -301,24 +250,6 @@ function discoverWorkspaceTypes(merged: Mod): WorkspaceTypeDecl[] {
301
250
  return out;
302
251
  }
303
252
 
304
- /** Scan merged exports for decls matching `match`: individual exports AND exported arrays. */
305
- function discover<T>(merged: Mod, match: (v: unknown) => v is T): T[] {
306
- const seen = new Set<string>();
307
- const out: T[] = [];
308
- const push = (v: T) => {
309
- const key = JSON.stringify(v, (_k, val) => (typeof val === "function" ? "<fn>" : val));
310
- if (!seen.has(key)) {
311
- seen.add(key);
312
- out.push(v);
313
- }
314
- };
315
- for (const v of Object.values(merged)) {
316
- if (match(v)) push(v);
317
- else if (Array.isArray(v)) for (const el of v) if (match(el)) push(el);
318
- }
319
- return out;
320
- }
321
-
322
253
  /** Validate the LITERAL dart-codegen import hints (config data, not module exports). */
323
254
  function checkDartImports(domainKey: string, imports: readonly DartImport[] | undefined): void {
324
255
  for (const imp of imports ?? []) {
@@ -532,6 +463,12 @@ async function main(): Promise<void> {
532
463
  const entryMintInstalls: string[] = []; // route-tagged in-plan mints (taxonomy packages only)
533
464
  const routingInput: Record<string, Mod[]> = {};
534
465
  const domainModules: DomainModule[] = [];
466
+ // THE SELF-DESCRIBE DECLS (G1): per domain key, the FINISHED plain read/maintenance decls the
467
+ // lump's `globalThis.describe` needs that it cannot rediscover by shape alone (mins/maxes are
468
+ // config-named; sharding sums are framework-appended). Plain JSON (finishCount/Sum/Extremum
469
+ // pass finished decls through), serialized into the generated entry as `openapiDecls`, so the
470
+ // kernel-run describe reproduces THIS compile's `<name>.openapi.json` byte-for-byte.
471
+ const openapiDeclsByDomain: Record<string, unknown> = {};
535
472
  const layered = layeredFlag || cfg.layered === true;
536
473
  const layeredDomains: { key: string; inputs: LayeredModuleInput[] }[] = [];
537
474
  let importSeq = 0;
@@ -831,6 +768,24 @@ async function main(): Promise<void> {
831
768
  // no-path / ambiguous / cross-home / unroutable law is a NAMED COMPILE
832
769
  // ERROR here — never a domain silently recorded as identity-excluded.
833
770
  const composed = domainModules[domainModules.length - 1]!;
771
+
772
+ // ── THE SELF-DESCRIBE DECLS (G1): the FINISHED plain read/maintenance decls the lump's
773
+ // `describe` reconstructs the openapi fragment from. Directives + aggregates are read from
774
+ // the live module exports IN the lump; these ride alongside (mins/maxes are config-named +
775
+ // sharding sums are framework-appended — not shape-rediscoverable). Omit-when-empty so a
776
+ // decl-free domain contributes nothing (and offers still derive from its directives).
777
+ {
778
+ const declsForDomain: Record<string, unknown[]> = {};
779
+ if (queries.length > 0) declsForDomain.queries = queries.map((q) => ({ id: q.id, key: [...q.key], returns: q.returns }));
780
+ if (counts.length > 0) declsForDomain.counts = counts.map((c) => finishCount(c));
781
+ if (sums.length > 0) declsForDomain.sums = sums.map((s) => finishSum(s));
782
+ if (mins.length > 0) declsForDomain.mins = mins.map((e) => finishExtremum(e));
783
+ if (maxes.length > 0) declsForDomain.maxes = maxes.map((e) => finishExtremum(e));
784
+ if (spatials.length > 0) declsForDomain.spatials = spatials.map((s) => ({ id: s.id, of: s.of, on: s.on }));
785
+ if (workspaceInvariants.length > 0) declsForDomain.workspaceInvariants = workspaceInvariants.map((w) => ({ id: w.id, on: w.on }));
786
+ if (Object.keys(declsForDomain).length > 0) openapiDeclsByDomain[d.key] = declsForDomain;
787
+ }
788
+
834
789
  if (composed.workspaceTypes !== undefined && composed.workspaceTypes.length > 0) {
835
790
  try {
836
791
  validateWorkspaceTaxonomyAndRoutes(composed);
@@ -948,10 +903,6 @@ async function main(): Promise<void> {
948
903
  const priorLock: StableIdLock | undefined = existsSync(lockPath)
949
904
  ? (JSON.parse(readFileSync(lockPath, "utf8")) as StableIdLock)
950
905
  : undefined;
951
- const priorBuildManifestsPath = path.join(outDir, "domain_identity_manifests.json");
952
- const priorBuildManifests: Record<string, string> | undefined = existsSync(priorBuildManifestsPath)
953
- ? (JSON.parse(readFileSync(priorBuildManifestsPath, "utf8")) as Record<string, string>)
954
- : undefined;
955
906
  const inferredRenames: { domain: string; renames: InferredRename[] }[] = [];
956
907
  const nextLock: StableIdLock = { v: 1, domains: {} };
957
908
  for (const mod of domainModules) {
@@ -977,23 +928,14 @@ async function main(): Promise<void> {
977
928
  // it carries no stable ids either.
978
929
  continue;
979
930
  }
980
- // Continuity source: the lockfile first (durable, committed), else the prior build.
931
+ // Continuity source: the committed nomos.stable-ids.json lockfile (the canonical identity ledger).
932
+ // (The old "else the prior build's domain_identity_manifests.json" fallback is gone with that sidecar.)
981
933
  let prior: StableIds | undefined;
982
934
  let priorSchemas: Record<string, WireSchema> | undefined;
983
935
  const locked = priorLock?.domains[identityName];
984
936
  if (locked !== undefined) {
985
937
  prior = locked.stableIds;
986
938
  priorSchemas = locked.schemas;
987
- } else if (priorBuildManifests?.[identityName] !== undefined) {
988
- prior = stableIdsFromManifestJson(priorBuildManifests[identityName]!);
989
- try {
990
- const parsed = JSON.parse(priorBuildManifests[identityName]!) as {
991
- aggregates?: { id: string; schema: WireSchema }[];
992
- };
993
- priorSchemas = Object.fromEntries((parsed.aggregates ?? []).map((a) => [a.id, a.schema]));
994
- } catch {
995
- priorSchemas = undefined;
996
- }
997
939
  }
998
940
  const { continuity, inferred } = deriveStableIdContinuity(
999
941
  identityName,
@@ -1132,6 +1074,14 @@ async function main(): Promise<void> {
1132
1074
  ...(entryReports.length > 0 ? [` reports: {`, ...entryReports, ` },`] : []),
1133
1075
  ...(eagerLump ? [] : [` routing: ${JSON.stringify(routing)},`]),
1134
1076
  ...(zodSeedNames.length > 0 ? [` seeds: [${zodSeedNames.join(", ")}],`] : []),
1077
+ // THE SELF-DESCRIBE INPUTS (G1): the finished read/maintenance decls + the package title, baked
1078
+ // into the lump so `globalThis.describe` reproduces this compile's `<name>.openapi.json` over the
1079
+ // FOLDED LAW — the kernel serves the spec, the host stages nothing. Omit-when-empty (decl-free
1080
+ // packages: offers still derive from directives; the title default is the workspace-level title).
1081
+ ...(Object.keys(openapiDeclsByDomain).length > 0
1082
+ ? [` openapiDecls: ${JSON.stringify(openapiDeclsByDomain)},`]
1083
+ : []),
1084
+ ` packageTitle: ${JSON.stringify(`${cfg.name} (nomos package)`)},`,
1135
1085
  `});`,
1136
1086
  // ROUTE-TAGGED IN-PLAN MINTS (sharding §4, slice 3): wraps globalThis.plan +
1137
1087
  // globalThis.nomos.mint at top-level eval — pre-freeze, after registerEngine's
@@ -1321,15 +1271,13 @@ async function main(): Promise<void> {
1321
1271
  layeredFileCount = relPaths.length;
1322
1272
  }
1323
1273
 
1324
- // ── 3. the READ manifest + IDENTITY artifacts ──
1325
- // These are generated from the same source functions embedded into USDA by emitUsd().
1326
- // They remain local diagnostics for now; the deploy/cloud/runtime contract ignores them.
1327
- const readManifest = buildReadManifest(domainModules);
1328
- const readPath = path.join(outDir, "domain_manifests.json");
1329
- writeFileSync(readPath, JSON.stringify(readManifest, null, 2) + "\n", "utf8");
1330
-
1274
+ // ── 3. the IDENTITY emit (for the summary's per-domain identityHash) ──
1275
+ // The admission identity is carried INSIDE the package USD IR (emitUsd) + the committed
1276
+ // nomos.stable-ids.json lockfile; the deploy/cloud/runtime contract uses neither a read-manifest nor an
1277
+ // identity-manifest sidecar. The old domain_manifests.json / domain_identity_*.json build files were
1278
+ // local diagnostics the contract ignored — REMOVED (they only confused /build consumers). We still
1279
+ // compute the identity to surface its per-domain hash in the summary below.
1331
1280
  const identity = buildIdentity(domainModules);
1332
- const { manifestsPath } = writeIdentity(identity, outDir);
1333
1281
 
1334
1282
  // ── 3b. the OpenAPI spec — THE PACKAGE'S SELF-DESCRIPTION (the host's runtime routing table;
1335
1283
  // architecture/githolon_host_contract.md "The Self-Describing API"). Each domain emits its fragment
@@ -1463,8 +1411,12 @@ async function main(): Promise<void> {
1463
1411
  ` layered ${rel(layeredRootPath)} (${layeredFileCount} module layer(s) in build/layers/ — flatten-verified byte-identical to the canonical IR)`,
1464
1412
  );
1465
1413
  }
1466
- console.log(` read ${rel(readPath)} (${Object.keys(readManifest.aggregateFieldKinds).length} aggregate(s), ${readManifest.queries.length} quer(y/ies), ${readManifest.counts.length} count(s), ${readManifest.sums?.length ?? 0} sum(s))`);
1467
- console.log(` identity ${rel(manifestsPath)} (${Object.keys(identity.manifests).length} domain(s)${identity.excluded.length ? `, ${identity.excluded.length} EXCLUDED (palette gap)` : ""})`);
1414
+ {
1415
+ const sum = (pick: (m: (typeof domainModules)[number]) => readonly unknown[] | undefined) =>
1416
+ domainModules.reduce((n, m) => n + (pick(m)?.length ?? 0), 0);
1417
+ console.log(` reads ${sum((m) => m.queries)} quer(y/ies), ${sum((m) => m.counts)} count(s), ${sum((m) => m.sums)} sum(s) — declared (routed via the openapi)`);
1418
+ }
1419
+ console.log(` identity ${Object.keys(identity.manifests).length} domain(s) (carried in the package IR + nomos.stable-ids.json; no sidecar files)${identity.excluded.length ? ` — ${identity.excluded.length} EXCLUDED (palette gap)` : ""}`);
1468
1420
  for (const [dom, h] of Object.entries(identity.hashes)) console.log(` ${dom.padEnd(20)} ${h}`);
1469
1421
  for (const ex of identity.excluded) console.log(` EXCLUDED ${ex.domain}: ${ex.reason}`);
1470
1422
  // STABLE IDS (#58): the minted/carried identity surface + every INFERRED rename —
@@ -0,0 +1,122 @@
1
+ // NOMOS — Nomos Sovereign: participants act · verify · remember LOCALLY; hosted remotes are
2
+ // replaceable custody/transport, not truth. ⇒ ONE Nomos GitHolon wasm32-wasip1 artifact.
3
+ // If a file isn't this / hosting this / authoring for this / proving this — it's gone.
4
+
5
+ /**
6
+ * decl_shapes.ts — THE DUCK-TYPE PREDICATES for the read/maintenance decl families, hoisted
7
+ * into ONE bundle-safe module so the COMPILER (`compile_package_main.ts`) and the LUMP's own
8
+ * self-describe verb (`engine_entry.ts` `describe`) discover decls by the EXACT same shape.
9
+ *
10
+ * Why shared: the kernel-served OpenAPI (G1 — openapi residue removal) is produced by the lump
11
+ * running `generateOpenApiFragment` over the SAME modules it dispatches; if the lump discovered
12
+ * queries/counts/sums/spatials by a DIFFERENT shape than the compiler did when it wrote
13
+ * `<name>.openapi.json`, the kernel-served spec would drift from the compiled one. One predicate
14
+ * set ⇒ one shape ⇒ byte-identity.
15
+ *
16
+ * ENGINE-BUNDLE-SAFE: imports NO node builtin (only DSL types) — it bundles under esbuild
17
+ * `--platform=neutral` into the sealed QuickJS lump.
18
+ */
19
+ import type { QueryDecl } from "./query.js";
20
+ import type { SpatialDecl } from "./spatial.js";
21
+ import type { AnySum } from "./sum.js";
22
+ import type { AnyCount } from "./count.js";
23
+ import type { WorkspaceInvariantDecl } from "./framework/workspace_invariant.js";
24
+
25
+ /** A merged domain-module exports bag (scanned by shape). */
26
+ export type DeclBag = Record<string, unknown>;
27
+
28
+ /** A declared query — `{id, key:string[], returns}` with no projection `fn` (that marks derived/combined). */
29
+ export function isQueryDecl(v: unknown): v is QueryDecl {
30
+ return (
31
+ !!v &&
32
+ typeof v === "object" &&
33
+ typeof (v as { id?: unknown }).id === "string" &&
34
+ Array.isArray((v as { key?: unknown }).key) &&
35
+ ((v as { key: unknown[] }).key as unknown[]).every((k) => typeof k === "string") &&
36
+ typeof (v as { returns?: unknown }).returns === "string" &&
37
+ typeof (v as { fn?: unknown }).fn !== "function"
38
+ );
39
+ }
40
+
41
+ /** A workspace invariant — branded `__isWorkspaceInvariant`. */
42
+ export function isWorkspaceInvariantDecl(v: unknown): v is WorkspaceInvariantDecl {
43
+ return (
44
+ !!v &&
45
+ typeof v === "object" &&
46
+ (v as { __isWorkspaceInvariant?: unknown }).__isWorkspaceInvariant === true
47
+ );
48
+ }
49
+
50
+ /** A spatial index — `{id, of, on}` with no projection `fn`. */
51
+ export function isSpatialDecl(v: unknown): v is SpatialDecl {
52
+ return (
53
+ !!v &&
54
+ typeof v === "object" &&
55
+ typeof (v as { id?: unknown }).id === "string" &&
56
+ typeof (v as { of?: unknown }).of === "string" &&
57
+ typeof (v as { on?: unknown }).on === "string" &&
58
+ typeof (v as { fn?: unknown }).fn !== "function"
59
+ );
60
+ }
61
+
62
+ /** A sum decl/builder — the `{id, of, sumField}` mark (counts/extremums never carry `sumField`). */
63
+ export function isSumDecl(v: unknown): v is AnySum {
64
+ if (!v || typeof v !== "object") return false;
65
+ const o = v as Record<string, unknown>;
66
+ return typeof o.id === "string" && typeof o.of === "string" && typeof o.sumField === "string";
67
+ }
68
+
69
+ /** A count decl/builder — `{id, of}` with NONE of the other decl families' marks. */
70
+ export function isCountDecl(v: unknown): v is AnyCount {
71
+ if (!v || typeof v !== "object") return false;
72
+ const o = v as Record<string, unknown>;
73
+ return (
74
+ typeof o.id === "string" &&
75
+ typeof o.of === "string" &&
76
+ typeof o.on !== "string" && // spatial
77
+ typeof o.fn !== "function" && // derived/combined
78
+ !Array.isArray(o.key) && // query
79
+ !("field" in o) && // extremum (wire-descriptor shape)
80
+ !("valueField" in o) && // extremum (the ExtremumDecl/Extremum-builder mark — never a count)
81
+ !("sumField" in o) && // sum (the SumDecl/Sum-builder mark — never a count)
82
+ !("orderBy" in o) && // ordered read
83
+ !("_existsMarker" in o) && // exists
84
+ typeof o.refField !== "string" && // combined
85
+ o.__isAggregateHandle !== true && // aggregate
86
+ typeof o.aggregateId !== "string" && // directive
87
+ typeof o.plan !== "function"
88
+ );
89
+ }
90
+
91
+ /** An extremum decl/builder — `{id, of, valueField}` with `{kind:"min"|"max"}` (the #47 read). */
92
+ export function isExtremumDecl(v: unknown): boolean {
93
+ if (!v || typeof v !== "object") return false;
94
+ const o = v as Record<string, unknown>;
95
+ return (
96
+ typeof o.id === "string" &&
97
+ typeof o.of === "string" &&
98
+ typeof o.valueField === "string" &&
99
+ (o.kind === "min" || o.kind === "max")
100
+ );
101
+ }
102
+
103
+ /**
104
+ * Scan merged exports for decls matching `match`: individual exports AND exported arrays, deduped
105
+ * by a function-blind JSON key (the SAME dedup `compile_package_main.ts` `discover` uses).
106
+ */
107
+ export function discover<T>(merged: DeclBag, match: (v: unknown) => v is T): T[] {
108
+ const seen = new Set<string>();
109
+ const out: T[] = [];
110
+ const push = (v: T) => {
111
+ const key = JSON.stringify(v, (_k, val) => (typeof val === "function" ? "<fn>" : val));
112
+ if (!seen.has(key)) {
113
+ seen.add(key);
114
+ out.push(v);
115
+ }
116
+ };
117
+ for (const v of Object.values(merged)) {
118
+ if (match(v)) push(v);
119
+ else if (Array.isArray(v)) for (const el of v) if (match(el)) push(el);
120
+ }
121
+ return out;
122
+ }
package/src/directive.ts CHANGED
@@ -141,6 +141,20 @@ export interface Directive<P = unknown> {
141
141
  * relation-free directive's canonical manifest is byte-identical to before this existed.
142
142
  */
143
143
  readonly declaredRelations: RelationDecl[];
144
+ /**
145
+ * BIRTH IS AN EXPLICIT, DECLARED KERNEL PRIMITIVE (G3 — the unix model). When `true`, this directive
146
+ * BIRTHS a child workspace as the defined CONSEQUENCE of its offer: its `plan` produces the child's
147
+ * genesis recipe (`birth(recipe)`) onto the intent's birth effect, and the kernel — reading THIS
148
+ * declaration off the law manifest (the `nomosBirthGate` era key, the SAME way it reads `.reads`/
149
+ * `.creates`/invariants — NOT domain state) — spawns the child's custody and self-validates its whole
150
+ * genesis through the child's OWN gate after this offer admits. The kernel NEVER scans a tenant/governance
151
+ * aggregate to detect a birth (a circular dependency, forbidden): the birth is declared, like a syscall.
152
+ * Lowered into the `nomosBirthGate` USD era key (emitted iff a package declares ≥1 birth directive), so
153
+ * pre-feature chains carry no key and replay byte-green (the era rule). `false` ⇒ a non-birthing directive.
154
+ * (FIELD `birthsChild` vs the chainable method `.births()` — the `requiresRelationGate`/`.requires()` and
155
+ * `attestationAuthorizedGate`/`.attestationAuthorized()` naming convention; the declaration is the noun.)
156
+ */
157
+ readonly birthsChild?: boolean;
144
158
  /**
145
159
  * The directive's DECLARED required HOST CAPABILITIES — the capability PORTS the HOST must
146
160
  * PROVIDE for this directive to run (`OFFER_KERNEL_ARCHITECTURE.dot` cluster_ports / `OFFER_FLOW.dot`
@@ -243,6 +257,16 @@ export interface RequirableDirective<P> extends Directive<P> {
243
257
  * Returns a NEW requirable directive.
244
258
  */
245
259
  requiresHostCapability(...portIds: string[]): RequirableDirective<P>;
260
+ /**
261
+ * Declare this directive a BIRTH primitive (G3 — the unix model): birthing a child workspace is the
262
+ * defined CONSEQUENCE of its offer. The `plan` must record the child's genesis recipe (`birth(recipe)`,
263
+ * reusing `buildGenesisChain`) onto the intent's birth effect; the kernel reads THIS declaration off the
264
+ * law manifest (the `nomosBirthGate` era key — NOT domain state) and, after the offer admits, spawns the
265
+ * child's custody + self-validates its genesis through the child's OWN gate. Purely additive +
266
+ * non-destructive — a directive that never calls this is NOT a birth primitive and is byte-identical in
267
+ * the canonical manifest to before this existed (the era rule). Returns a NEW requirable directive.
268
+ */
269
+ births(): RequirableDirective<P>;
246
270
  }
247
271
 
248
272
  /** Stage 1: id + a referential marker (which fixes the target aggregate). */
@@ -409,6 +433,12 @@ function makeRequirable<P>(base: Directive<P>): RequirableDirective<P> {
409
433
  const merged = [...new Set([...base.declaredHostCapabilities, ...portIds])].sort();
410
434
  return makeRequirable({ ...base, declaredHostCapabilities: merged });
411
435
  },
436
+ births(): RequirableDirective<P> {
437
+ // BIRTH AS A DECLARED KERNEL PRIMITIVE (G3): mark this directive a birth — its plan's `birth(recipe)`
438
+ // rides the intent, and the kernel (reading the `nomosBirthGate` era key, not domain state) executes
439
+ // the birth after admit. Era-ruled: a non-birthing directive omits the flag and is byte-identical.
440
+ return makeRequirable({ ...base, birthsChild: true });
441
+ },
412
442
  };
413
443
  }
414
444