@githolon/dsl 0.17.0 → 0.19.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.
@@ -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 —
@@ -1500,8 +1452,8 @@ async function main(): Promise<void> {
1500
1452
  console.log(` deploy ${rel(deployPath)}`);
1501
1453
  console.log(``);
1502
1454
  console.log(`deploy (Nomos Cloud):`);
1503
- console.log(` githolon ws create <ws> && githolon deploy <ws> # the secret is saved + sent for you`);
1504
- console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'Authorization: Bearer <workspaceSecret>' \\`);
1455
+ console.log(` githolon login --agent && githolon ws create <ws> && githolon deploy <ws> # no secret — deploy is OPEN, kernel-judged at the offer gate`);
1456
+ console.log(` # raw lane: curl -X POST -H 'content-type: application/json' -H 'x-nomos-principal: <your-auth-uid>' \\`);
1505
1457
  console.log(` # --data-binary @${rel(deployPath)} https://nomos.captainapp.co.uk/v2/workspaces/<ws>/domains`);
1506
1458
  if (proofPath !== undefined) {
1507
1459
  console.log(`prove it OFFLINE first: npx githolon proof # the generated proof's offline legs on a LOCAL holon (no cloud workspace)`);
@@ -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
 
@@ -54,6 +54,13 @@
54
54
  import { executeDirectiveToIntent } from "./wire_encode.js";
55
55
  import { expandCapabilityExports } from "./capability_exports.js";
56
56
  import { RelationTuple } from "./framework/relations.js";
57
+ import { generateOpenApiFragment, mergeOpenApiFragments, type OpenApiDoc } from "./codegen_openapi.js";
58
+ import type { QueryDecl } from "./query.js";
59
+ import type { AnyCount } from "./count.js";
60
+ import type { AnySum } from "./sum.js";
61
+ import type { AnyExtremum } from "./extremum.js";
62
+ import type { SpatialDecl } from "./spatial.js";
63
+ import type { DomainModule } from "./codegen_dart.js";
57
64
  import type { Directive } from "./directive.js";
58
65
  import type { AggregateHandle, AggregateInvariantFn, AggregateInvariantVerdict } from "./aggregate.js";
59
66
  import type { Ports } from "./ctx.js";
@@ -120,6 +127,34 @@ export interface EngineEntryConfig {
120
127
  * `sideEffects: false` (a bare side-effect import would be tree-shaken away).
121
128
  */
122
129
  readonly seeds?: readonly unknown[];
130
+ /**
131
+ * THE SELF-DESCRIBE INPUT (G1 — openapi residue removal): the per-domain read/maintenance
132
+ * decls the OpenAPI fragment needs that are NOT discoverable by shape from the live module
133
+ * exports alone (queries/counts/sums/mins/maxes/spatials/workspaceInvariants — the compiler
134
+ * already auto-discovered + named-resolved them when it composed each DomainModule). The
135
+ * directives + aggregates ARE discovered from the modules; these ride alongside so the
136
+ * lump's `globalThis.describe` reproduces the EXACT `<name>.openapi.json` the compiler wrote
137
+ * (byte-identity, no Rust re-implementation, no drift). Keyed by domain dispatch key, in the
138
+ * same declaration order as `domains`. Omitted ⇒ describe still emits offers from directives.
139
+ */
140
+ readonly openapiDecls?: Record<string, OpenApiDomainDecls>;
141
+ /**
142
+ * The package's OpenAPI document title (the compiler's `"<name> (nomos package)"`). Baked into
143
+ * the lump so `describe` reproduces the compiler's `<name>.openapi.json` byte-for-byte without
144
+ * the kernel knowing the package name. The kernel MAY override it (the workspace-level title).
145
+ */
146
+ readonly packageTitle?: string;
147
+ }
148
+
149
+ /** The openapi-relevant decls one domain contributes beyond its directives/aggregates. */
150
+ export interface OpenApiDomainDecls {
151
+ readonly queries?: readonly QueryDecl[];
152
+ readonly counts?: readonly AnyCount[];
153
+ readonly sums?: readonly AnySum[];
154
+ readonly mins?: readonly AnyExtremum[];
155
+ readonly maxes?: readonly AnyExtremum[];
156
+ readonly spatials?: readonly SpatialDecl[];
157
+ readonly workspaceInvariants?: readonly WorkspaceInvariantDecl[];
123
158
  }
124
159
 
125
160
  interface RegistryEntry {
@@ -395,6 +430,8 @@ function portsFromHost(principalRaw = ""): Ports {
395
430
  export interface RegisteredEngine {
396
431
  plan(job: unknown): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[] };
397
432
  planReport(job: unknown): string;
433
+ /** The G1 self-describe verb: the lump's own merged OpenAPI, derived from its modules. */
434
+ describe(opts?: { lawHash?: string; title?: string }): OpenApiDoc;
398
435
  }
399
436
 
400
437
  /**
@@ -543,7 +580,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
543
580
  /** `{workspaceInvariantReads:{id}, payload}` → the bounded ref-set `[{name,aggregate,id},…]`. */
544
581
  function planWorkspaceInvariantReads(job: {
545
582
  intent?: { workspaceInvariantReads?: { id?: string }; payload?: unknown };
546
- capturedPorts?: { principal?: string };
583
+ capturedPorts?: { principal?: string; principal_verified?: boolean };
547
584
  }): string {
548
585
  const id = job.intent?.workspaceInvariantReads?.id;
549
586
  if (typeof id !== "string") {
@@ -559,14 +596,17 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
559
596
  // LAW-GATED AUTHZ: the captured author identity (a top-level sibling of `intent`, set by the wasm
560
597
  // author leg + replayed verbatim) — the governance invariant reads it to gate by the AUTHOR.
561
598
  const principal = (job.capturedPorts?.principal ?? "") as string;
562
- return JSON.stringify(decl.reads({ intent, principal }));
599
+ // VA-core (Phase 3): the kernel-DERIVED author-verified flag (`captured_ports.principal_verified`),
600
+ // threaded the same way as `principal`, so an invariant may gate on a VERIFIED author (e.g. enrollSignerSelf).
601
+ const principalVerified = job.capturedPorts?.principal_verified === true;
602
+ return JSON.stringify(decl.reads({ intent, principal, principalVerified }));
563
603
  }
564
604
 
565
605
  /** `{workspaceInvariant:{id,on}, payload?}` + priorState (named snapshots) → {accept}|{reject}. */
566
606
  function planWorkspaceInvariantAssert(job: {
567
607
  intent?: { workspaceInvariant?: { id?: string }; payload?: unknown };
568
608
  priorState?: Record<string, unknown>;
569
- capturedPorts?: { principal?: string };
609
+ capturedPorts?: { principal?: string; principal_verified?: boolean };
570
610
  }): string {
571
611
  const id = job.intent?.workspaceInvariant?.id;
572
612
  if (typeof id !== "string") {
@@ -581,7 +621,9 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
581
621
  const snapshots = (job.priorState ?? {}) as Record<string, Record<string, unknown>>;
582
622
  const intent = (job.intent?.payload ?? {}) as Record<string, unknown>;
583
623
  const principal = (job.capturedPorts?.principal ?? "") as string;
584
- return JSON.stringify(decl.assert(snapshots, { intent, principal }));
624
+ // VA-core (Phase 3): the kernel-DERIVED author-verified flag — threaded like `principal`.
625
+ const principalVerified = job.capturedPorts?.principal_verified === true;
626
+ return JSON.stringify(decl.assert(snapshots, { intent, principal, principalVerified }));
585
627
  }
586
628
 
587
629
  function planDerive(job: {
@@ -717,7 +759,7 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
717
759
  function plan(job: {
718
760
  intent?: { domain?: string; directiveId?: string; payload?: unknown };
719
761
  capturedPorts?: { principal?: string };
720
- }): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[] } {
762
+ }): WireEvent[] | { events: WireEvent[]; strikes?: string[]; endorse?: { sink: string; facet: string; reason: string | null }[]; births?: { workspace: string; genesisChain: unknown }[] } {
721
763
  const intent = job.intent ?? {};
722
764
  const domain = intent.domain;
723
765
  const directiveId = intent.directiveId;
@@ -739,18 +781,77 @@ export function registerEngine(config: EngineEntryConfig): RegisteredEngine {
739
781
  // it to check the law's nomosFlowGate.endorse; decode_plan_output ignores it for the byte-compare),
740
782
  // so a principal-free intent still emits the bare array, byte-identical.
741
783
  const endorse = wire.endorse;
742
- if (!wire.strikes.length && !(endorse && endorse.length)) return wire.events;
784
+ // THE BIRTH EFFECT (G3): a `.births()` directive's plan recorded the child's genesis recipe; it rides the
785
+ // plan-output object as a TRANSIENT verify-input (like `endorse` — the kernel reads it onto the intent's
786
+ // `captured_ports.births` and folds it after admit; decode_plan_output ignores it for the byte-compare),
787
+ // so a birth-free intent still emits the bare event array, byte-identical (the era rule).
788
+ const births = wire.births;
789
+ if (!wire.strikes.length && !(endorse && endorse.length) && !(births && births.length)) return wire.events;
743
790
  return {
744
791
  events: wire.events,
745
792
  ...(wire.strikes.length ? { strikes: wire.strikes } : {}),
746
793
  ...(endorse && endorse.length ? { endorse } : {}),
794
+ ...(births && births.length ? { births } : {}),
747
795
  };
748
796
  }
749
797
 
798
+ /**
799
+ * THE SELF-DESCRIBE VERB (G1) — the lump's OWN OpenAPI, derived from the SAME modules it
800
+ * dispatches. The kernel runs this over the FOLDED law (the lump IS `nomos:executable:
801
+ * javascriptHex` in the package USDA in custody), so the workspace's API surface is a pure,
802
+ * deterministic function of installed law — NOT a host-staged blob. It reproduces, byte for
803
+ * byte, the `<name>.openapi.json` the compiler wrote: per domain (declaration order) build the
804
+ * openapi-relevant DomainModule (directives + aggregates from the live exports; queries/counts/
805
+ * sums/mins/maxes/spatials/workspaceInvariants from `config.openapiDecls`), `generateOpenApi
806
+ * Fragment`, then `mergeOpenApiFragments` — the EXACT calls compile_package_main makes.
807
+ * `opts.lawHash`/`opts.title` are injected by the kernel at describe-time (the version stamp +
808
+ * package title), matching the compiler's `domainHash`/`"<name> (nomos package)"`.
809
+ */
810
+ function describe(opts?: { lawHash?: string; title?: string }): OpenApiDoc {
811
+ const lawHash = opts?.lawHash;
812
+ const fragments: OpenApiDoc[] = [];
813
+ for (const domainName of domainNames) {
814
+ const sources = config.domains[domainName];
815
+ if (sources === undefined) continue;
816
+ const merged = mergeModules(sources.map((m) => (typeof m === "function" ? m() : m)));
817
+ // Aggregates dedup by wire id + RelationTuple auto-ensure (the compose contract) — openapi
818
+ // reads aggregates only for the hasMany inverse indexes, so this matches composeDomainModule.
819
+ const byId = aggregatesOf(merged);
820
+ const directives = [...directivesOf(merged).values()];
821
+ if (!byId.has(RelationTuple.id) && directives.some((d) => d.aggregateId === RelationTuple.id)) {
822
+ byId.set(RelationTuple.id, RelationTuple);
823
+ }
824
+ const decls = config.openapiDecls?.[domainName] ?? {};
825
+ const module = {
826
+ name: domainName,
827
+ domain: domainName,
828
+ aggregates: [...byId.values()],
829
+ directives,
830
+ ...(decls.queries && decls.queries.length > 0 ? { queries: [...decls.queries] } : {}),
831
+ ...(decls.counts && decls.counts.length > 0 ? { counts: [...decls.counts] } : {}),
832
+ ...(decls.sums && decls.sums.length > 0 ? { sums: [...decls.sums] } : {}),
833
+ ...(decls.mins && decls.mins.length > 0 ? { mins: [...decls.mins] } : {}),
834
+ ...(decls.maxes && decls.maxes.length > 0 ? { maxes: [...decls.maxes] } : {}),
835
+ ...(decls.spatials && decls.spatials.length > 0 ? { spatials: [...decls.spatials] } : {}),
836
+ ...(decls.workspaceInvariants && decls.workspaceInvariants.length > 0
837
+ ? { workspaceInvariants: [...decls.workspaceInvariants] }
838
+ : {}),
839
+ } as unknown as DomainModule;
840
+ fragments.push(generateOpenApiFragment(module, lawHash !== undefined ? { lawHash } : {}));
841
+ }
842
+ const title = opts?.title ?? config.packageTitle;
843
+ return mergeOpenApiFragments(fragments, {
844
+ ...(title !== undefined ? { title } : {}),
845
+ ...(lawHash !== undefined ? { lawHash } : {}),
846
+ });
847
+ }
848
+
750
849
  // The engine evals the bundle as a classic script and FREEZES globalThis before
751
- // dispatch — assign both entries NOW, at top-level eval.
850
+ // dispatch — assign all entries NOW, at top-level eval. `describe` is the G1 self-
851
+ // describe verb (the kernel `describe` run mode evals `globalThis.describe(opts)`).
752
852
  (globalThis as { plan?: unknown }).plan = plan;
753
853
  (globalThis as { planReport?: unknown }).planReport = planReport;
854
+ (globalThis as { describe?: unknown }).describe = describe;
754
855
 
755
- return { plan, planReport } as RegisteredEngine;
856
+ return { plan, planReport, describe } as RegisteredEngine;
756
857
  }