@githolon/dsl 0.17.0 → 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/dart/lib/nomos_client.dart +5 -0
- package/dart/lib/src/errors.dart +79 -0
- package/dart/lib/src/home.dart +20 -0
- package/dart/lib/src/webview_bridge.dart +68 -0
- package/dart/lib/src/wire.dart +117 -0
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -1
- package/dart/test/home_parity_test.dart +9 -0
- package/dart/test/wire_test.dart +48 -0
- package/package.json +1 -1
- package/src/births.ts +82 -0
- package/src/build_package.ts +23 -27
- package/src/codegen_dart.ts +10 -0
- package/src/compile_package_main.ts +61 -109
- package/src/decl_shapes.ts +122 -0
- package/src/directive.ts +30 -0
- package/src/engine_entry.ts +109 -8
- package/src/framework/home.ts +179 -0
- package/src/framework/impure_capability.ts +8 -4
- package/src/framework/relations.ts +22 -0
- package/src/framework/signing_identity.ts +291 -2
- package/src/framework/workspace_invariant.ts +17 -2
- package/src/framework/workspaces.ts +193 -30
- package/src/index.ts +42 -0
- package/src/usd.ts +44 -0
- package/src/usd_layers.ts +41 -0
- package/src/wire.ts +13 -0
- package/src/wire_encode.ts +14 -2
|
@@ -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
|
|
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
|
|
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
|
|
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
|
-
|
|
197
|
-
|
|
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
|
|
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
|
|
1325
|
-
//
|
|
1326
|
-
//
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
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
|
-
|
|
1467
|
-
|
|
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
|
|
package/src/engine_entry.ts
CHANGED
|
@@ -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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
}
|