@githolon/dsl 0.103.12 → 0.103.13
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/CHANGELOG.md +5 -0
- package/dart/lib/src/runner/node-entry.mjs +234 -170
- package/dart/lib/src/runner/offline-install.mjs +87 -112
- package/dart/lib/src/webview_bridge.dart +11 -11
- package/dart/pubspec.yaml +1 -1
- package/dart/test/webview_bridge_test.dart +10 -7
- package/dart_types/CHANGELOG.md +1 -1
- package/dart_types/lib/nomos_types.dart +4 -4
- package/dart_types/lib/src/versioned_nomos_ref.dart +13 -11
- package/package.json +1 -1
- package/runtime/generated-application-host.js +424 -237
- package/src/aggregate.ts +3 -1
- package/src/build_package.ts +77 -6
- package/src/check_package_main.ts +39 -16
- package/src/codegen_dart.ts +185 -48
- package/src/codegen_dart_application.ts +82 -16
- package/src/codegen_dart_client.ts +38 -10
- package/src/codegen_dot.ts +3 -3
- package/src/codegen_proof.ts +24 -2
- package/src/codegen_ts.ts +32 -19
- package/src/compile_package_main.ts +68 -11
- package/src/derived.ts +17 -0
- package/src/directive.ts +65 -0
- package/src/engine_entry.ts +22 -2
- package/src/enum_type.ts +71 -0
- package/src/fields.ts +135 -5
- package/src/flow_gate.ts +2 -1
- package/src/framework/workspaces.ts +73 -0
- package/src/idof_lint.ts +5 -1
- package/src/index.ts +20 -0
- package/src/kernel_capabilities.ts +12 -1
- package/src/law_check.ts +107 -28
- package/src/law_graph.ts +151 -20
- package/src/law_graph_usda.ts +23 -0
- package/src/law_value_type.ts +10 -3
- package/src/lsp_server.ts +26 -16
- package/src/package_config.ts +112 -9
- package/src/package_feature_topology.ts +174 -0
- package/src/plan_source_slice.ts +6 -0
- package/src/report.ts +6 -0
- package/src/stable_ids_for_module.ts +1 -0
- package/src/value_object.ts +9 -2
- package/src/workspace_sharding.ts +4 -4
|
@@ -63,6 +63,7 @@ import {
|
|
|
63
63
|
type PermissionVocabulary,
|
|
64
64
|
} from "./codegen_dart.js";
|
|
65
65
|
import type { ContractHandle } from "./contract.js";
|
|
66
|
+
import { bindEnumTypeExports } from "./enum_type.js";
|
|
66
67
|
import { generateDartBridgeClient } from "./codegen_dart_client.js";
|
|
67
68
|
import {
|
|
68
69
|
dartApplicationClassName,
|
|
@@ -179,10 +180,11 @@ import {
|
|
|
179
180
|
import {
|
|
180
181
|
validateNomosPackageConfig,
|
|
181
182
|
type NomosPackageConfig,
|
|
182
|
-
type
|
|
183
|
+
type NormalizedNomosPackageDomainConfig,
|
|
183
184
|
} from "./package_config.js";
|
|
184
185
|
import { assertUsdaRelationshipIntegrity } from "./usda_relationship_integrity.js";
|
|
185
186
|
import { assertPublicPackageImports } from "./package_source_imports.js";
|
|
187
|
+
import { assertPackageFeatureTopology } from "./package_feature_topology.js";
|
|
186
188
|
import { packageDependencyUsageFindings } from "./package_dependency_usage.js";
|
|
187
189
|
import {
|
|
188
190
|
PlanSourceSliceProgram,
|
|
@@ -224,7 +226,7 @@ const BUSINESS_BIRTH_FRAMEWORK_PLAN_SOURCES: Readonly<Record<string, PlanSourceS
|
|
|
224
226
|
};
|
|
225
227
|
const BUSINESS_BIRTH_FRAMEWORK_PLAN_DOMAIN = "__nomos_birth_genesis_plan_v1";
|
|
226
228
|
|
|
227
|
-
interface DomainConfig extends
|
|
229
|
+
interface DomainConfig extends NormalizedNomosPackageDomainConfig {
|
|
228
230
|
readonly key: string;
|
|
229
231
|
readonly name?: string;
|
|
230
232
|
readonly modules: readonly string[];
|
|
@@ -821,7 +823,7 @@ const DART_SUPPORT_CORE_SRC = [
|
|
|
821
823
|
"foreign_ref.dart",
|
|
822
824
|
"activity.dart",
|
|
823
825
|
"nomos_ref.dart",
|
|
824
|
-
// Immutable publication pin + stable series and the required
|
|
826
|
+
// Immutable publication pin + stable series and the required version-status sum
|
|
825
827
|
// type. Pure data; the generated application owns the effectful check.
|
|
826
828
|
"versioned_nomos_ref.dart",
|
|
827
829
|
// Optional first-class references are explicit required choices in generated
|
|
@@ -1583,8 +1585,8 @@ function emitDart(
|
|
|
1583
1585
|
// The generated clients import their runtime support directly. Keeping it out of the public barrel is
|
|
1584
1586
|
// the surface firewall: a tenant import receives business types and application APIs, never raw runtime.
|
|
1585
1587
|
supportImport === "vendored"
|
|
1586
|
-
? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef, NomosOptionalRef, VersionedNomosRef,
|
|
1587
|
-
: `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef,
|
|
1588
|
+
? `export '${genRel}/src/${VENDORED_TYPES_SHIM}' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport};`
|
|
1589
|
+
: `export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport};`,
|
|
1588
1590
|
...(contractsImportUri !== undefined
|
|
1589
1591
|
? tenantDartExport(`${genRel}/${CONTRACTS_FILE}.dart`, sourceByFile.get(CONTRACTS_FILE) ?? "")
|
|
1590
1592
|
: []),
|
|
@@ -1650,8 +1652,8 @@ const NOMOS_TYPES_DEP = "^0.8.4";
|
|
|
1650
1652
|
// 0.48.x: session-bound clients (XClient.bind/NomosClientInterface/DomainClientSessionMismatch/
|
|
1651
1653
|
// DomainNotInstalledForSession, engine.follow, pendingConflicts lanes) — the generated client
|
|
1652
1654
|
// REQUIRES this surface. 0.48.1 fixes NomosSession.watchPendingConflicts delegation.
|
|
1653
|
-
const NOMOS_CLIENT_DEP = "^0.64.
|
|
1654
|
-
const NOMOS_FLUTTER_DEP = "^0.63.
|
|
1655
|
+
const NOMOS_CLIENT_DEP = "^0.64.14";
|
|
1656
|
+
const NOMOS_FLUTTER_DEP = "^0.63.17";
|
|
1655
1657
|
// DRIFT TRIPWIRE: pubspec_pins.test.ts asserts these equal the CURRENT versions in dsl/dart/pubspec.yaml
|
|
1656
1658
|
// and dsl/dart_types/pubspec.yaml — a Dart release without this bump fails the suite, so tenants never
|
|
1657
1659
|
// land back in dependency_overrides (the exact co2 report this closes, third occurrence).
|
|
@@ -2028,9 +2030,22 @@ function dartAliasLines(names: Readonly<Record<string, string>> | undefined, dec
|
|
|
2028
2030
|
];
|
|
2029
2031
|
for (const [generated, alias] of Object.entries(names).sort(([a], [b]) => (a < b ? -1 : 1))) {
|
|
2030
2032
|
if (!declared.has(generated)) {
|
|
2033
|
+
const semanticTail = generated.match(/[A-Z][a-z0-9]+(?:[A-Z][a-z0-9]+)?$/u)?.[0];
|
|
2034
|
+
const nearby = [...declared]
|
|
2035
|
+
.filter((name) =>
|
|
2036
|
+
name.includes("AssetFieldRequirement") ||
|
|
2037
|
+
(generated.endsWith("FieldType") && name.endsWith("FieldType")) ||
|
|
2038
|
+
(semanticTail !== undefined && name.endsWith(semanticTail)))
|
|
2039
|
+
.sort((left, right) => {
|
|
2040
|
+
const leftExactKind = generated.endsWith("FieldType") && left.endsWith("FieldType") ? 0 : 1;
|
|
2041
|
+
const rightExactKind = generated.endsWith("FieldType") && right.endsWith("FieldType") ? 0 : 1;
|
|
2042
|
+
return leftExactKind - rightExactKind || left.localeCompare(right);
|
|
2043
|
+
})
|
|
2044
|
+
.slice(0, 20);
|
|
2031
2045
|
fail(
|
|
2032
2046
|
`dart.names: '${generated}' is not a generated public type in this package's TYPES surface — ` +
|
|
2033
|
-
`check the spelling against the emitted src/generated/*.dart (aliases cover types, not client classes)
|
|
2047
|
+
`check the spelling against the emitted src/generated/*.dart (aliases cover types, not client classes)` +
|
|
2048
|
+
(nearby.length === 0 ? "" : `; nearby generated types: ${nearby.join(", ")}`),
|
|
2034
2049
|
);
|
|
2035
2050
|
}
|
|
2036
2051
|
if (!/^[A-Z][A-Za-z0-9_]*$/.test(alias)) {
|
|
@@ -2294,7 +2309,18 @@ function emitDartPackages(
|
|
|
2294
2309
|
...(dd.modForDart.dartImports ?? []),
|
|
2295
2310
|
...[...frImports].filter((uri) => !(dd.modForDart.dartImports ?? []).some((i) => i.uri === uri)).sort().map((uri) => ({ uri })),
|
|
2296
2311
|
];
|
|
2297
|
-
const
|
|
2312
|
+
const typedDomain = merged.length > 0
|
|
2313
|
+
? { ...dd.modForDart, dartImports: merged }
|
|
2314
|
+
: dd.modForDart;
|
|
2315
|
+
// A capability declaration is canonical law, but its provider dispatcher is
|
|
2316
|
+
// effectful runtime machinery (TaskProvider, HostClock, NomosIntent). Layered
|
|
2317
|
+
// packages deliberately keep the types package pure, so never inject that
|
|
2318
|
+
// adapter into a frontend data library merely because the law can emit an
|
|
2319
|
+
// obligation. Provider SDKs belong to an explicitly selected provider target;
|
|
2320
|
+
// flat/legacy generation retains its existing opt-in behavior.
|
|
2321
|
+
const modForTypes = (typedDomain.impureCapabilities?.length ?? 0) === 0
|
|
2322
|
+
? typedDomain
|
|
2323
|
+
: { ...typedDomain, impureCapabilities: [] };
|
|
2298
2324
|
writeTypes(
|
|
2299
2325
|
mod.name,
|
|
2300
2326
|
generateDartDomain(modForTypes, {
|
|
@@ -2372,7 +2398,7 @@ function emitDartPackages(
|
|
|
2372
2398
|
businessSharedPath,
|
|
2373
2399
|
[
|
|
2374
2400
|
"// GENERATED by nomos-compile — shared business values and activity; no runtime machinery.",
|
|
2375
|
-
`export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef,
|
|
2401
|
+
`export 'package:nomos_types/nomos_types.dart' show NomosRef, NomosOptionalRef, VersionedNomosRef, ReferenceVersionStatus, ReferenceVersionCurrent, ReferenceVersionOutdated, ReferenceVersionUnavailable${sha256DigestExport}${gitObjectIdExport}, Identity, User, Recipient, Invitation, ActivityEvent, ActivityRef, ActivitySeverity, SurfacedObservation, SurfacedValue, SurfacedResolved, SurfacedPending, SurfacedEvolutionRequired;`,
|
|
2376
2402
|
...(sharedBusinessTypeNames.length === 0
|
|
2377
2403
|
? []
|
|
2378
2404
|
: [`export '${FRAMEWORK_FILE}.dart' show ${sharedBusinessTypeNames.join(", ")};`]),
|
|
@@ -2870,13 +2896,31 @@ async function main(): Promise<void> {
|
|
|
2870
2896
|
if (Object.prototype.hasOwnProperty.call(cfg, "layered")) {
|
|
2871
2897
|
fail("nomos.project.ts no longer accepts 'layered': every law closure is layered, with no alternate compile lane");
|
|
2872
2898
|
}
|
|
2899
|
+
let sourceTopologyDiagnostics: ReturnType<typeof assertPackageFeatureTopology> = [];
|
|
2873
2900
|
try {
|
|
2901
|
+
sourceTopologyDiagnostics = assertPackageFeatureTopology(cfg.domains, cfgDir);
|
|
2874
2902
|
assertPublicPackageImports(
|
|
2875
2903
|
cfg.domains.flatMap((domain) => domain.modules.map((module) => path.resolve(cfgDir, module))),
|
|
2876
2904
|
);
|
|
2877
2905
|
} catch (error) {
|
|
2878
2906
|
fail(`package import boundary refused: ${(error as Error).message}`);
|
|
2879
2907
|
}
|
|
2908
|
+
if (sourceTopologyDiagnostics.length > 0) {
|
|
2909
|
+
const heading =
|
|
2910
|
+
`nomos-compile: ${sourceTopologyDiagnostics.length} unacknowledged source-shape warning(s)`;
|
|
2911
|
+
const lines = sourceTopologyDiagnostics.map((diagnostic) =>
|
|
2912
|
+
` ⚠ [${diagnostic.rule}] ${diagnostic.domain}/${diagnostic.feature} ` +
|
|
2913
|
+
`${diagnostic.source}: ${diagnostic.message} — fix: ${diagnostic.fix}`
|
|
2914
|
+
);
|
|
2915
|
+
if (preflightFlag) {
|
|
2916
|
+
console.error(`${heading}; preflight requires an explicit fix or per-file acknowledgement`);
|
|
2917
|
+
for (const line of lines) console.error(line);
|
|
2918
|
+
process.exitCode = 1;
|
|
2919
|
+
return;
|
|
2920
|
+
}
|
|
2921
|
+
console.warn(heading);
|
|
2922
|
+
for (const line of lines) console.warn(line);
|
|
2923
|
+
}
|
|
2880
2924
|
// EVERY PUBLISHED APPLICATION IS A COMPLETE PRODUCT ENTRYPOINT. `rootPart` is the explicit OpenUSD
|
|
2881
2925
|
// application/package boundary: only that independently published root privately receives the authority
|
|
2882
2926
|
// bootstrap law which turns verified credential evidence into durable device authorship. Ordinary law
|
|
@@ -2920,6 +2964,7 @@ async function main(): Promise<void> {
|
|
|
2920
2964
|
const abs = path.resolve(cfgDir, rel);
|
|
2921
2965
|
if (!existsSync(abs)) fail(`domain '${domain.key}': module not found: ${abs}`);
|
|
2922
2966
|
const imported = await import(pathToFileURL(abs).href) as Mod;
|
|
2967
|
+
bindEnumTypeExports(imported, abs);
|
|
2923
2968
|
authored = { ...authored, ...imported };
|
|
2924
2969
|
for (const [exportName, value] of Object.entries(imported)) {
|
|
2925
2970
|
if (value !== cfg.application?.homeGenesis || !isApplicationHomeGenesis(value)) continue;
|
|
@@ -2949,6 +2994,10 @@ async function main(): Promise<void> {
|
|
|
2949
2994
|
? []
|
|
2950
2995
|
: [{
|
|
2951
2996
|
key: APPLICATION_HOME_DOMAIN,
|
|
2997
|
+
features: [{
|
|
2998
|
+
key: "application-home",
|
|
2999
|
+
modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
|
|
3000
|
+
}],
|
|
2952
3001
|
modules: [path.join(DSL_DIR, "src", "framework", "application_home.ts")],
|
|
2953
3002
|
}]),
|
|
2954
3003
|
];
|
|
@@ -3088,6 +3137,7 @@ async function main(): Promise<void> {
|
|
|
3088
3137
|
const abs = path.resolve(cfgDir, rel);
|
|
3089
3138
|
if (!existsSync(abs)) fail(`domain '${d.key}': module not found: ${abs}`);
|
|
3090
3139
|
const m = (await import(pathToFileURL(abs).href)) as Mod;
|
|
3140
|
+
bindEnumTypeExports(m, abs);
|
|
3091
3141
|
mods.push(m);
|
|
3092
3142
|
sourceExprs.push(`() => require(${JSON.stringify(abs)})`);
|
|
3093
3143
|
for (const [exportName, value] of Object.entries(m)) {
|
|
@@ -4055,9 +4105,15 @@ async function main(): Promise<void> {
|
|
|
4055
4105
|
planInputDigestCache.set(absolute, digest);
|
|
4056
4106
|
return digest;
|
|
4057
4107
|
};
|
|
4108
|
+
const planSourceSliceImplementation = fileURLToPath(new URL("./plan_source_slice.ts", import.meta.url));
|
|
4058
4109
|
const planCompilerIdentity = planBytesDigest(Buffer.concat([
|
|
4059
4110
|
Buffer.from("nomos.plan-compiler.v1\0", "utf8"),
|
|
4060
4111
|
readFileSync(fileURLToPath(import.meta.url)),
|
|
4112
|
+
// The slicer decides which bindings and imports enter every cached plan. A slicer fix without
|
|
4113
|
+
// this input otherwise replays bytecode produced by the old dependency closure indefinitely.
|
|
4114
|
+
...(existsSync(planSourceSliceImplementation)
|
|
4115
|
+
? [Buffer.from("\0plan-source-slice\0", "utf8"), readFileSync(planSourceSliceImplementation)]
|
|
4116
|
+
: []),
|
|
4061
4117
|
Buffer.from(`\0esbuild:${esbuildMod.version}\0`, "utf8"),
|
|
4062
4118
|
]));
|
|
4063
4119
|
let planSequence = 0;
|
|
@@ -5039,6 +5095,7 @@ async function main(): Promise<void> {
|
|
|
5039
5095
|
const requiresKernel = ((): string[] => {
|
|
5040
5096
|
const probes: FeatureProbeModule[] = domainModules.map((m) => ({
|
|
5041
5097
|
...(m.queries ? { queries: m.queries } : {}),
|
|
5098
|
+
...(m.deriveds ? { deriveds: m.deriveds } : {}),
|
|
5042
5099
|
...(m.surfacedResolvers ? { surfacedResolvers: m.surfacedResolvers } : {}),
|
|
5043
5100
|
...(m.aggregates ? { aggregates: m.aggregates } : {}),
|
|
5044
5101
|
}));
|
|
@@ -5177,7 +5234,7 @@ async function main(): Promise<void> {
|
|
|
5177
5234
|
const temporaryDeclarationPath = temporaryClientPath.replace(/\.ts$/, ".d.ts");
|
|
5178
5235
|
const runtimeTypesSource = [
|
|
5179
5236
|
`export declare function nomosInternalOpenGeneratedApplication<T extends object>(`,
|
|
5180
|
-
` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; onPerformance?: (sample: { readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance"; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) => void; lawHash: string },`,
|
|
5237
|
+
` options: { cloud: string; application: string; signIn: (request?: { readonly deviceChallenge?: string }) => Promise<string>; onPerformance?: (sample: { readonly operation: "open" | "offer" | "birth" | "read" | "sync" | "maintenance"; readonly name: string; readonly durationMs: number; readonly domain?: string; readonly directive?: string }) => void; lawHash: string; readyCommands?: readonly { readonly domain: string; readonly packageHash: string; readonly directiveId: string }[] },`,
|
|
5181
5238
|
` bind: (holon: unknown, user: { readonly identityId: string; readonly identityAddress: string }) => T,`,
|
|
5182
5239
|
`): Promise<T & {`,
|
|
5183
5240
|
` currentStatus(): Promise<any>;`,
|
package/src/derived.ts
CHANGED
|
@@ -33,6 +33,7 @@
|
|
|
33
33
|
*/
|
|
34
34
|
import type { z } from "zod";
|
|
35
35
|
import type { AggregateHandle } from "./aggregate.js";
|
|
36
|
+
import type { TemporalEnumTransition } from "./fields.js";
|
|
36
37
|
|
|
37
38
|
/**
|
|
38
39
|
* The pure derive function: maps ONE aggregate's folded fields (a plain JSON object — the
|
|
@@ -59,6 +60,22 @@ export interface DerivedDecl {
|
|
|
59
60
|
readonly fn: DeriveFn;
|
|
60
61
|
}
|
|
61
62
|
|
|
63
|
+
/** A clock-aware enum projection compiled entirely as typed data and evaluated natively by the
|
|
64
|
+
* kernel. It deliberately has no function body: source, missing state and monotone thresholds are
|
|
65
|
+
* the complete executable algebra. Instances are synthesized from `t.timestamp().temporalEnum(...)`. */
|
|
66
|
+
export interface TemporalEnumDecl {
|
|
67
|
+
readonly kind: "temporalEnum";
|
|
68
|
+
readonly id: string;
|
|
69
|
+
readonly of: string;
|
|
70
|
+
readonly sourceField: string;
|
|
71
|
+
readonly missing: string;
|
|
72
|
+
readonly initial: string;
|
|
73
|
+
readonly transitions: readonly TemporalEnumTransition[];
|
|
74
|
+
readonly returns: z.ZodTypeAny;
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
export type AnyDerivedDecl = DerivedDecl | TemporalEnumDecl;
|
|
78
|
+
|
|
62
79
|
/**
|
|
63
80
|
* The {@link DerivedOf} BUILDER: it has named its `of`-type, so `.as(...)` (which fixes
|
|
64
81
|
* the pure fn and yields the finished {@link DerivedDecl}) is available. This is the ONLY
|
package/src/directive.ts
CHANGED
|
@@ -24,6 +24,7 @@ import type { Ports } from "./ctx.js";
|
|
|
24
24
|
import type { RelationDecl } from "./relation.js";
|
|
25
25
|
import type { QueryDecl } from "./query.js";
|
|
26
26
|
import { normalizeActivity, type ActivityInput, type ActivityDescriptor } from "./activity.js";
|
|
27
|
+
import { idOfMetaOf } from "./value_object.js";
|
|
27
28
|
import { BIRTH_METER_QUERIES } from "./birth_meter.js";
|
|
28
29
|
import { zodDef, zodKind, zodObjectShape } from "./zod_introspection.js";
|
|
29
30
|
import {
|
|
@@ -50,6 +51,16 @@ import {
|
|
|
50
51
|
|
|
51
52
|
export type ReferentialMarker = "creates" | "mutates" | "ensures" | "archives";
|
|
52
53
|
|
|
54
|
+
/**
|
|
55
|
+
* The existing record selected by a mutation/archive. This is application-shape
|
|
56
|
+
* metadata, not law: custody still carries the ordinary typed payload field,
|
|
57
|
+
* while generated clients expose the reference separately from business facts.
|
|
58
|
+
*/
|
|
59
|
+
export interface DirectiveTargetBinding {
|
|
60
|
+
readonly field: string;
|
|
61
|
+
readonly aggregateId: string;
|
|
62
|
+
}
|
|
63
|
+
|
|
53
64
|
/**
|
|
54
65
|
* A DECLARATIVE WRITE-PATH PRECONDITION (the precondition-primitives arc). One descriptor per
|
|
55
66
|
* `.requiresExisting` / `.creates(Agg).payload(...).withId(idFn)` create-once / `.where` marker on a directive. Each
|
|
@@ -217,6 +228,8 @@ export interface Directive<P = unknown> {
|
|
|
217
228
|
readonly manifestOnly?: true;
|
|
218
229
|
/** Typed declaration target retained for compiler composition; omitted by every wire/law serializer. */
|
|
219
230
|
readonly aggregate?: AggregateHandle;
|
|
231
|
+
/** Generated-API target binding. Omitted from canonical law/wire output. */
|
|
232
|
+
readonly targetBinding?: DirectiveTargetBinding;
|
|
220
233
|
/**
|
|
221
234
|
* Business-result cardinality for the primary marker target. The default is
|
|
222
235
|
* exactly one; `.affectsMany()` and `.affectsOptional()` opt into the other
|
|
@@ -795,6 +808,7 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
795
808
|
private readonly payloadSchema: z.ZodType<P>,
|
|
796
809
|
private readonly createOnceIdField?: (payload: unknown) => string,
|
|
797
810
|
private readonly preconditions: PreconditionDescriptor[] = [],
|
|
811
|
+
private readonly targetField?: string,
|
|
798
812
|
) {}
|
|
799
813
|
|
|
800
814
|
/** Non-destructive copy carrying one more precondition (and/or the create-once id field). */
|
|
@@ -809,6 +823,38 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
809
823
|
this.payloadSchema,
|
|
810
824
|
createOnceIdField ?? this.createOnceIdField,
|
|
811
825
|
desc !== undefined ? [...this.preconditions, desc] : this.preconditions,
|
|
826
|
+
this.targetField,
|
|
827
|
+
);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/**
|
|
831
|
+
* Select the existing aggregate reference targeted by this mutation/archive.
|
|
832
|
+
* Normally unnecessary: a single top-level `idOf(Target)` field is inferred.
|
|
833
|
+
* Use this only when the payload carries more than one reference to the same
|
|
834
|
+
* aggregate and the compiler cannot choose without domain knowledge.
|
|
835
|
+
*/
|
|
836
|
+
target<K extends Extract<keyof P, string>>(field: K): DirectivePlanStep<Id, P> {
|
|
837
|
+
if (this.marker !== "mutates" && this.marker !== "archives") {
|
|
838
|
+
throw new Error(
|
|
839
|
+
`directive('${this.id}').target(...) is only valid on .mutates(...) or .archives(...)`,
|
|
840
|
+
);
|
|
841
|
+
}
|
|
842
|
+
const shape = zodObjectShape(this.payloadSchema);
|
|
843
|
+
const schema = shape[field];
|
|
844
|
+
const actual = schema === undefined ? undefined : idOfMetaOf(schema);
|
|
845
|
+
if (actual !== this.aggregate.id) {
|
|
846
|
+
throw new Error(
|
|
847
|
+
`directive('${this.id}').target('${field}') must name an idOf(${shortName(this.aggregate.id)}) payload field`,
|
|
848
|
+
);
|
|
849
|
+
}
|
|
850
|
+
return new DirectivePlanStep(
|
|
851
|
+
this.id,
|
|
852
|
+
this.marker,
|
|
853
|
+
this.aggregate,
|
|
854
|
+
this.payloadSchema,
|
|
855
|
+
this.createOnceIdField,
|
|
856
|
+
this.preconditions,
|
|
857
|
+
field,
|
|
812
858
|
);
|
|
813
859
|
}
|
|
814
860
|
|
|
@@ -968,10 +1014,28 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
968
1014
|
...this.preconditions.slice(0, -1),
|
|
969
1015
|
{ ...last, declassify: trimmed },
|
|
970
1016
|
],
|
|
1017
|
+
this.targetField,
|
|
971
1018
|
);
|
|
972
1019
|
}
|
|
973
1020
|
|
|
974
1021
|
plan(fn: (payload: P, ctx: Ports) => PlannedOp[]): RequirableDirective<P> {
|
|
1022
|
+
let targetBinding: DirectiveTargetBinding | undefined;
|
|
1023
|
+
if (this.marker === "mutates" || this.marker === "archives") {
|
|
1024
|
+
const shape = zodObjectShape(this.payloadSchema);
|
|
1025
|
+
const candidates = Object.entries(shape)
|
|
1026
|
+
.filter(([, schema]) => idOfMetaOf(schema) === this.aggregate.id)
|
|
1027
|
+
.map(([field]) => field);
|
|
1028
|
+
const field = this.targetField ?? (candidates.length === 1 ? candidates[0] : undefined);
|
|
1029
|
+
if (this.targetField === undefined && candidates.length > 1) {
|
|
1030
|
+
throw new Error(
|
|
1031
|
+
`directive('${this.id}') has several idOf(${shortName(this.aggregate.id)}) payload fields; ` +
|
|
1032
|
+
`select the mutation target with .target('field') before .plan(...)`,
|
|
1033
|
+
);
|
|
1034
|
+
}
|
|
1035
|
+
if (field !== undefined) {
|
|
1036
|
+
targetBinding = { field, aggregateId: this.aggregate.id };
|
|
1037
|
+
}
|
|
1038
|
+
}
|
|
975
1039
|
return makeRequirable({
|
|
976
1040
|
id: this.id,
|
|
977
1041
|
marker: this.marker,
|
|
@@ -979,6 +1043,7 @@ class DirectivePlanStep<Id extends string, P> {
|
|
|
979
1043
|
aggregate: this.aggregate,
|
|
980
1044
|
payloadSchema: this.payloadSchema,
|
|
981
1045
|
plan: fn,
|
|
1046
|
+
...(targetBinding === undefined ? {} : { targetBinding }),
|
|
982
1047
|
declaredReads: [],
|
|
983
1048
|
declaredQueryReads: [],
|
|
984
1049
|
declaredEmits: {},
|
package/src/engine_entry.ts
CHANGED
|
@@ -52,7 +52,7 @@
|
|
|
52
52
|
* tenant package for the long-form rationale of each branch.
|
|
53
53
|
*/
|
|
54
54
|
import { executeDirectiveToIntent } from "./wire_encode.js";
|
|
55
|
-
import { expandCapabilityExports } from "./capability_exports.js";
|
|
55
|
+
import { expandCapabilityExports, isCapabilityDecl } from "./capability_exports.js";
|
|
56
56
|
import { RelationTuple, __setDispatchContext } from "./framework/relations.js";
|
|
57
57
|
import { BIRTH_METER_AGGREGATES } from "./birth_meter.js";
|
|
58
58
|
import { AuthProvider as FrameworkAuthProvider, setAuthProvider as frameworkSetAuthProvider } from "./framework/auth_provider_law.js";
|
|
@@ -193,7 +193,8 @@ function aggregatesOf(mod: DomainModuleExports): Map<string, AggregateHandle> {
|
|
|
193
193
|
if (
|
|
194
194
|
v &&
|
|
195
195
|
typeof v === "object" &&
|
|
196
|
-
(v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true
|
|
196
|
+
(v as { __isAggregateHandle?: boolean }).__isAggregateHandle === true &&
|
|
197
|
+
(v as { __isForwardRef?: boolean }).__isForwardRef !== true
|
|
197
198
|
) {
|
|
198
199
|
const h = v as AggregateHandle;
|
|
199
200
|
out.set(h.id, h);
|
|
@@ -332,6 +333,25 @@ function buildDomainSlice(mod: DomainModuleExports, domain: string): DomainSlice
|
|
|
332
333
|
const executableDirectives = applicationLanguage === undefined
|
|
333
334
|
? discoveredDirectives
|
|
334
335
|
: executableDirectivesForLanguage(applicationLanguage, discoveredDirectives, domain);
|
|
336
|
+
// Closed application languages hide their private consequence fragments from
|
|
337
|
+
// generated tenant clients. Capability lifecycle plans are nevertheless
|
|
338
|
+
// kernel-executable law: an atomic public action can create an obligation and
|
|
339
|
+
// a provider must later author its typed receipt. Mirror package composition
|
|
340
|
+
// by retaining the exact framework-derived lifecycle here as well.
|
|
341
|
+
for (const value of Object.values(mod)) {
|
|
342
|
+
if (!isCapabilityDecl(value)) continue;
|
|
343
|
+
for (const directive of [
|
|
344
|
+
value.obligation,
|
|
345
|
+
value.complete,
|
|
346
|
+
value.fail,
|
|
347
|
+
value.block,
|
|
348
|
+
value.deadLetter,
|
|
349
|
+
] as Directive<unknown>[]) {
|
|
350
|
+
if (!executableDirectives.some((candidate) => candidate.id === directive.id)) {
|
|
351
|
+
executableDirectives.push(directive);
|
|
352
|
+
}
|
|
353
|
+
}
|
|
354
|
+
}
|
|
335
355
|
const dirs = new Map(executableDirectives.map((directive) => [directive.id, directive]));
|
|
336
356
|
// A directive already owns the typed aggregate handle it targets. Retain that
|
|
337
357
|
// link unconditionally in the EXECUTABLE slice instead of requiring a second
|
package/src/enum_type.ts
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
import { z } from "zod";
|
|
2
|
+
|
|
3
|
+
const NOMOS_ENUM_TYPE_NAME = Symbol.for("nomos.enumType.name");
|
|
4
|
+
|
|
5
|
+
export interface EnumType<E extends readonly [string, ...string[]]> {
|
|
6
|
+
readonly __isEnumType: true;
|
|
7
|
+
readonly values: E;
|
|
8
|
+
readonly zod: z.ZodType<E[number]>;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* One scalar vocabulary reused across fields, payloads and value objects.
|
|
13
|
+
*
|
|
14
|
+
* Its canonical name is the exported TypeScript binding. Authors do not repeat that binding as a
|
|
15
|
+
* string: `export const CustomFieldType = enumType([...])` is the declaration and the compiler
|
|
16
|
+
* binds `CustomFieldType` while loading the configured source module.
|
|
17
|
+
*/
|
|
18
|
+
export function enumType<const E extends readonly [string, ...string[]]>(
|
|
19
|
+
values: E,
|
|
20
|
+
): EnumType<E> {
|
|
21
|
+
const schema = z.enum(values) as unknown as z.ZodType<E[number]>;
|
|
22
|
+
return Object.freeze({ __isEnumType: true as const, values, zod: schema });
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
export function enumTypeNameOf(schema: z.ZodTypeAny): string | undefined {
|
|
26
|
+
const name = (schema as unknown as Record<symbol, unknown>)[NOMOS_ENUM_TYPE_NAME];
|
|
27
|
+
return typeof name === "string" ? name : undefined;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export function isEnumType(value: unknown): value is EnumType<readonly [string, ...string[]]> {
|
|
31
|
+
return value !== null && typeof value === "object" &&
|
|
32
|
+
(value as { __isEnumType?: unknown }).__isEnumType === true;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Compiler/LSP seam: bind semantic identity from the language's own exported symbol table. */
|
|
36
|
+
export function bindEnumTypeExport(
|
|
37
|
+
value: unknown,
|
|
38
|
+
exportName: string,
|
|
39
|
+
source = "configured law module",
|
|
40
|
+
): void {
|
|
41
|
+
if (!isEnumType(value)) return;
|
|
42
|
+
if (!/^[A-Z][A-Za-z0-9]*$/u.test(exportName)) {
|
|
43
|
+
throw new Error(
|
|
44
|
+
`${source} exports enumType(...) as '${exportName}'; semantic type exports must be UpperCamelCase`,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
const existing = enumTypeNameOf(value.zod);
|
|
48
|
+
if (existing !== undefined && existing !== exportName) {
|
|
49
|
+
throw new Error(
|
|
50
|
+
`${source} exports one enumType(...) as both '${existing}' and '${exportName}'; ` +
|
|
51
|
+
"a semantic type has one canonical exported identity",
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
if (existing === undefined) {
|
|
55
|
+
Object.defineProperty(value.zod, NOMOS_ENUM_TYPE_NAME, {
|
|
56
|
+
value: exportName,
|
|
57
|
+
enumerable: false,
|
|
58
|
+
configurable: false,
|
|
59
|
+
writable: false,
|
|
60
|
+
});
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export function bindEnumTypeExports(
|
|
65
|
+
exports: Record<string, unknown>,
|
|
66
|
+
source = "configured law module",
|
|
67
|
+
): void {
|
|
68
|
+
for (const [exportName, value] of Object.entries(exports)) {
|
|
69
|
+
bindEnumTypeExport(value, exportName, source);
|
|
70
|
+
}
|
|
71
|
+
}
|