@githolon/dsl 0.103.11 → 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 +10 -0
- package/dart/lib/src/runner/node-entry.mjs +234 -170
- package/dart/lib/src/runner/offline-install.mjs +111 -123
- 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
package/src/fields.ts
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
* the value type (via Zod) is preserved.
|
|
16
16
|
*/
|
|
17
17
|
import { z } from "zod";
|
|
18
|
+
import { isEnumType, type EnumType } from "./enum_type.js";
|
|
18
19
|
import { Conflict, Counter, FirstWriterWins, LastPosition, type Driver } from "./drivers.js";
|
|
19
20
|
import type { AggregateHandle } from "./aggregate.js";
|
|
20
21
|
import type { ContractHandle } from "./contract.js";
|
|
@@ -292,7 +293,13 @@ export type ValueObjectFieldDescriptor =
|
|
|
292
293
|
* was declared through `unit(...)` — the generated Dart extension-type wrapper's name. */
|
|
293
294
|
| { readonly type: "number"; readonly int?: true; readonly unit?: string; readonly optional?: true }
|
|
294
295
|
| { readonly type: "boolean"; readonly optional?: true }
|
|
295
|
-
| {
|
|
296
|
+
| {
|
|
297
|
+
readonly type: "enum";
|
|
298
|
+
readonly values: readonly string[];
|
|
299
|
+
/** Stable authored scalar name from enumType(...), when this is shared vocabulary. */
|
|
300
|
+
readonly name?: string;
|
|
301
|
+
readonly optional?: true;
|
|
302
|
+
}
|
|
296
303
|
/** A `z.literal(<string>)` — a CONSTANT discriminator-style member (generated as a getter). */
|
|
297
304
|
| { readonly type: "literal"; readonly value: string; readonly optional?: true }
|
|
298
305
|
| { readonly type: "array"; readonly items: ValueObjectFieldDescriptor; readonly optional?: true }
|
|
@@ -540,6 +547,11 @@ export interface Field<T = unknown, K extends FieldKind = FieldKind> {
|
|
|
540
547
|
/** Business-declared source contracts this field can understand during a gradual rollout.
|
|
541
548
|
* Compiler-owned metadata; generated clients and tenant runtime code never see contract ids. */
|
|
542
549
|
readonly evolutions?: readonly FieldEvolutionDeclaration[];
|
|
550
|
+
/** TRUE for `t.timestamp()`: the physical wire remains an int, while the business value is an
|
|
551
|
+
* epoch-millisecond timestamp and may own compiler-generated temporal enum reads. */
|
|
552
|
+
readonly isTimestamp?: true;
|
|
553
|
+
/** Temporal enum projections declared directly on this timestamp. Present only on `t.timestamp()`. */
|
|
554
|
+
readonly temporalEnums?: readonly TemporalEnumFieldDeclaration[];
|
|
543
555
|
/** Returns a copy with the chosen driver while preserving fluent law markers already applied. */
|
|
544
556
|
merge(driver: Driver): this;
|
|
545
557
|
/** Tag this field's facet, threaded through merge/optional (the canonical form is the
|
|
@@ -629,6 +641,8 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
629
641
|
clearRequires?: string;
|
|
630
642
|
clearRequiresObject?: string;
|
|
631
643
|
evolutions?: readonly FieldEvolutionDeclaration[];
|
|
644
|
+
timestamp?: true;
|
|
645
|
+
temporalEnums?: readonly TemporalEnumFieldDeclaration[];
|
|
632
646
|
} = {},
|
|
633
647
|
): Field<T, K> {
|
|
634
648
|
const isOptional = extra.isOptional ?? false;
|
|
@@ -661,6 +675,10 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
661
675
|
...(extra.evolutions !== undefined && extra.evolutions.length > 0
|
|
662
676
|
? { evolutions: extra.evolutions }
|
|
663
677
|
: {}),
|
|
678
|
+
...(extra.timestamp ? { isTimestamp: true as const } : {}),
|
|
679
|
+
...(extra.temporalEnums !== undefined && extra.temporalEnums.length > 0
|
|
680
|
+
? { temporalEnums: extra.temporalEnums }
|
|
681
|
+
: {}),
|
|
664
682
|
merge(d: Driver): typeof f {
|
|
665
683
|
return makeField<T, K>(kind, zod, d, extra) as typeof f;
|
|
666
684
|
},
|
|
@@ -775,6 +793,62 @@ function makeField<T, K extends FieldKind = FieldKind>(
|
|
|
775
793
|
return makeField<T, K>(kind, zod, driver, { ...extra, facet }) as Field<T, K> & { readonly facet: Fa };
|
|
776
794
|
},
|
|
777
795
|
};
|
|
796
|
+
if (extra.timestamp) {
|
|
797
|
+
(f as Field<T, K> & {
|
|
798
|
+
temporalEnum(id: string, states: TemporalEnumStates<string>): Field<T, K>;
|
|
799
|
+
}).temporalEnum = (id, states) => {
|
|
800
|
+
if (!/^[A-Za-z][A-Za-z0-9_]{0,127}$/.test(id)) {
|
|
801
|
+
throw new Error(
|
|
802
|
+
`t.timestamp().temporalEnum(): read id must be an identifier (letter, then letters/digits/_); got ${JSON.stringify(id)}`,
|
|
803
|
+
);
|
|
804
|
+
}
|
|
805
|
+
if (typeof states.missing !== "string" || states.missing.length === 0 ||
|
|
806
|
+
typeof states.initial !== "string" || states.initial.length === 0) {
|
|
807
|
+
throw new Error(
|
|
808
|
+
`t.timestamp().temporalEnum('${id}'): missing and initial must be non-empty enum values`,
|
|
809
|
+
);
|
|
810
|
+
}
|
|
811
|
+
const transitions: TemporalEnumTransition[] = [];
|
|
812
|
+
let previous = 0;
|
|
813
|
+
let previousValue = states.initial;
|
|
814
|
+
const values = new Set<string>([states.missing, states.initial]);
|
|
815
|
+
for (const [afterMillis, value] of states.after) {
|
|
816
|
+
if (!Number.isSafeInteger(afterMillis) || afterMillis <= previous) {
|
|
817
|
+
throw new Error(
|
|
818
|
+
`t.timestamp().temporalEnum('${id}'): thresholds must be strictly increasing positive safe integer milliseconds; ` +
|
|
819
|
+
`got ${JSON.stringify(afterMillis)} after ${previous}`,
|
|
820
|
+
);
|
|
821
|
+
}
|
|
822
|
+
if (typeof value !== "string" || value.length === 0) {
|
|
823
|
+
throw new Error(`t.timestamp().temporalEnum('${id}'): every transition needs a non-empty enum value`);
|
|
824
|
+
}
|
|
825
|
+
if (value === previousValue) {
|
|
826
|
+
throw new Error(
|
|
827
|
+
`t.timestamp().temporalEnum('${id}'): transition at ${afterMillis}ms repeats ${JSON.stringify(value)}; remove the no-op threshold`,
|
|
828
|
+
);
|
|
829
|
+
}
|
|
830
|
+
if (values.has(value)) {
|
|
831
|
+
throw new Error(
|
|
832
|
+
`t.timestamp().temporalEnum('${id}'): enum value ${JSON.stringify(value)} is reused; age states must progress monotonically`,
|
|
833
|
+
);
|
|
834
|
+
}
|
|
835
|
+
values.add(value);
|
|
836
|
+
transitions.push({ afterMillis, value });
|
|
837
|
+
previous = afterMillis;
|
|
838
|
+
previousValue = value;
|
|
839
|
+
}
|
|
840
|
+
if ((extra.temporalEnums ?? []).some((candidate) => candidate.id === id)) {
|
|
841
|
+
throw new Error(`t.timestamp().temporalEnum('${id}'): this timestamp already declares that read id`);
|
|
842
|
+
}
|
|
843
|
+
return makeField<T, K>(kind, zod, driver, {
|
|
844
|
+
...extra,
|
|
845
|
+
temporalEnums: [
|
|
846
|
+
...(extra.temporalEnums ?? []),
|
|
847
|
+
{ id, missing: states.missing, initial: states.initial, transitions },
|
|
848
|
+
],
|
|
849
|
+
});
|
|
850
|
+
};
|
|
851
|
+
}
|
|
778
852
|
// LAZY REF TARGET: when the target was given as a thunk (`t.ref(() => Agg)` / `t.hasMany(() => Agg)` — to
|
|
779
853
|
// break a bidirectional declaration cycle), expose `refAggregateId` as a MEMOIZED getter that resolves the
|
|
780
854
|
// thunk on first read (build time, after all module consts init). Consumers read `refAggregateId` as usual.
|
|
@@ -902,6 +976,46 @@ export interface IntRules {
|
|
|
902
976
|
readonly maximum?: number;
|
|
903
977
|
}
|
|
904
978
|
|
|
979
|
+
/** One monotone age threshold in a temporal enum projection. `afterMillis` is measured from the
|
|
980
|
+
* stored timestamp; crossing it changes the read value without writing business state. */
|
|
981
|
+
export interface TemporalEnumTransition {
|
|
982
|
+
readonly afterMillis: number;
|
|
983
|
+
readonly value: string;
|
|
984
|
+
}
|
|
985
|
+
|
|
986
|
+
/** Compiler-owned declaration attached to a timestamp field by `.temporalEnum(...)`. The aggregate
|
|
987
|
+
* composition seam turns this into a first-class read declaration automatically, so the author never
|
|
988
|
+
* has to export/register a second object. */
|
|
989
|
+
export interface TemporalEnumFieldDeclaration {
|
|
990
|
+
readonly id: string;
|
|
991
|
+
readonly missing: string;
|
|
992
|
+
readonly initial: string;
|
|
993
|
+
readonly transitions: readonly TemporalEnumTransition[];
|
|
994
|
+
}
|
|
995
|
+
|
|
996
|
+
export interface TemporalEnumStates<E extends string> {
|
|
997
|
+
/** Value when the source timestamp has never been observed. Exhaustive by construction. */
|
|
998
|
+
readonly missing: E;
|
|
999
|
+
/** Value from the source timestamp until the first threshold. */
|
|
1000
|
+
readonly initial: E;
|
|
1001
|
+
/** Strictly increasing `[ageInMilliseconds, value]` transitions. */
|
|
1002
|
+
readonly after: readonly (readonly [number, E])[];
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
/** An epoch-millisecond business timestamp. It keeps the kernel's efficient integer wire but exposes
|
|
1006
|
+
* the safe temporal projection vocabulary directly through autocomplete. */
|
|
1007
|
+
export interface TimestampField<T = number> extends Field<T, "int"> {
|
|
1008
|
+
readonly isTimestamp: true;
|
|
1009
|
+
readonly temporalEnums?: readonly TemporalEnumFieldDeclaration[];
|
|
1010
|
+
merge(driver: Driver): this;
|
|
1011
|
+
optional(): TimestampField<T | undefined>;
|
|
1012
|
+
fromBirth(): this & { readonly isFromBirth: true };
|
|
1013
|
+
temporalEnum<const E extends string>(
|
|
1014
|
+
id: string,
|
|
1015
|
+
states: TemporalEnumStates<E>,
|
|
1016
|
+
): TimestampField<T>;
|
|
1017
|
+
}
|
|
1018
|
+
|
|
905
1019
|
function naturalRule(value: number | undefined, name: string): void {
|
|
906
1020
|
if (value !== undefined && (!Number.isSafeInteger(value) || value < 0)) {
|
|
907
1021
|
throw new Error(`${name} must be a non-negative safe integer; got ${JSON.stringify(value)}`);
|
|
@@ -954,6 +1068,17 @@ export const t = {
|
|
|
954
1068
|
if (maximum !== undefined) schema = schema.max(maximum);
|
|
955
1069
|
return makeField("int", schema, FirstWriterWins);
|
|
956
1070
|
},
|
|
1071
|
+
/** A UTC timestamp stored canonically as non-negative epoch milliseconds. Unlike a bare `t.int()`,
|
|
1072
|
+
* this progressively exposes `.temporalEnum(...)`, making time-dependent categories a typed,
|
|
1073
|
+
* kernel-owned read instead of an arbitrary callback or UI calculation. */
|
|
1074
|
+
timestamp(): TimestampField<number> {
|
|
1075
|
+
return makeField<number, "int">(
|
|
1076
|
+
"int",
|
|
1077
|
+
z.number().int().nonnegative(),
|
|
1078
|
+
FirstWriterWins,
|
|
1079
|
+
{ timestamp: true },
|
|
1080
|
+
) as TimestampField<number>;
|
|
1081
|
+
},
|
|
957
1082
|
/** A boolean flag (Lww). Stored as the kernel scalar `"true"`/`"false"` and decoded back to a real
|
|
958
1083
|
* JSON boolean on read — so a flag is `t.bool()`, not a 2-value `t.enum`. Counts/filters key on the
|
|
959
1084
|
* same string form. (Not encryptable — only Lww-class scalars are, and bool already is.) */
|
|
@@ -988,10 +1113,15 @@ export const t = {
|
|
|
988
1113
|
return z.array(el);
|
|
989
1114
|
},
|
|
990
1115
|
/** Enum via `as const` literal union — value typos squeal at compile time. */
|
|
991
|
-
enum<const E extends readonly [string, ...string[]]>(
|
|
1116
|
+
enum<const E extends readonly [string, ...string[]]>(
|
|
1117
|
+
values: E | EnumType<E>,
|
|
1118
|
+
): Field<E[number], "enum"> {
|
|
1119
|
+
const schema = isEnumType(values)
|
|
1120
|
+
? values.zod
|
|
1121
|
+
: z.enum(values) as unknown as z.ZodType<E[number]>;
|
|
992
1122
|
return makeField<E[number], "enum">(
|
|
993
1123
|
"enum",
|
|
994
|
-
|
|
1124
|
+
schema,
|
|
995
1125
|
FirstWriterWins,
|
|
996
1126
|
);
|
|
997
1127
|
},
|
|
@@ -1161,7 +1291,7 @@ export const t = {
|
|
|
1161
1291
|
},
|
|
1162
1292
|
});
|
|
1163
1293
|
},
|
|
1164
|
-
/** A version pin whose
|
|
1294
|
+
/** A version pin whose version status is checked against one law-declared current pointer on a stable series.
|
|
1165
1295
|
* The declaration fails immediately if `series.current` is not an atomic ref to `target`. */
|
|
1166
1296
|
versionedForeignRef(opts: {
|
|
1167
1297
|
target: AggregateHandle<string, Record<string, Field>> | ContractHandle;
|
|
@@ -1178,7 +1308,7 @@ export const t = {
|
|
|
1178
1308
|
if (current.nomosRef?.targetAggregateId !== opts.target.id) {
|
|
1179
1309
|
throw new Error(
|
|
1180
1310
|
`t.versionedForeignRef: '${opts.series.id}.${opts.current}' must be ` +
|
|
1181
|
-
`t.nomosRef(${opts.target.id}); it is the O(1) current pointer used for
|
|
1311
|
+
`t.nomosRef(${opts.target.id}); it is the O(1) current pointer used for version status`,
|
|
1182
1312
|
);
|
|
1183
1313
|
}
|
|
1184
1314
|
const targetIsContract = (opts.target as { __isContract?: unknown }).__isContract === true;
|
package/src/flow_gate.ts
CHANGED
|
@@ -235,7 +235,8 @@ export function assertFlowGate(m: FlowLaw, mod?: FlowModule): void {
|
|
|
235
235
|
}
|
|
236
236
|
|
|
237
237
|
// ── RULE 5 — NO PRIVATE BYTES IN AN OUTBOUND OFFER (Send's external send) ────
|
|
238
|
-
// The
|
|
238
|
+
// The kernel's due-offer read exposes typed bytes from the authenticated main frontier's outbox/
|
|
239
|
+
// tree for POSTing to a capability endpoint —
|
|
239
240
|
// a DIFFERENT observer set (the external adapter). If the capability's task aggregate is
|
|
240
241
|
// private (requires a read cap) or encrypted, those bytes cross the confidentiality boundary.
|
|
241
242
|
// Refuse unless declassified. CURRENTLY VACUOUS BY CONSTRUCTION (capability task aggregates are
|
|
@@ -471,6 +471,27 @@ export const CapabilityBinding = aggregate("CapabilityBinding", {
|
|
|
471
471
|
status: t.enum(GRANT_STATUSES).merge(Lww),
|
|
472
472
|
}).public();
|
|
473
473
|
|
|
474
|
+
/**
|
|
475
|
+
* CapabilityAddress — the custodied destination for an addressed capability.
|
|
476
|
+
*
|
|
477
|
+
* An address is public routing configuration, not a credential. Keeping it in
|
|
478
|
+
* workspace state lets one compiled sovereign package serve production, dev and
|
|
479
|
+
* self-hosted workspaces without hard-coding one tenant's infrastructure into law.
|
|
480
|
+
* The kernel reads this fact only to address a sealed outbound offer; the host
|
|
481
|
+
* still receives opaque bytes and cannot choose or rewrite the destination.
|
|
482
|
+
*/
|
|
483
|
+
export const CapabilityAddress = aggregate("CapabilityAddress", {
|
|
484
|
+
capability: t.string().merge(Lww),
|
|
485
|
+
endpoint: t.string().merge(Lww),
|
|
486
|
+
configuredBy: t.string().merge(Lww),
|
|
487
|
+
configuredAt: t.int().merge(Lww),
|
|
488
|
+
status: t.enum(GRANT_STATUSES).merge(Lww),
|
|
489
|
+
}).public();
|
|
490
|
+
|
|
491
|
+
export function capabilityAddressId(capability: string): string {
|
|
492
|
+
return `capability-address:${capability}`;
|
|
493
|
+
}
|
|
494
|
+
|
|
474
495
|
// --- workspace directives ---------------------------------------------------------
|
|
475
496
|
|
|
476
497
|
// ── THE ONE BIRTH RECORD (the `kind`-collapse — architecture/provisioning_as_obligations.md) ───────────
|
|
@@ -1524,6 +1545,53 @@ export const revokeCapability = directive("revokeCapability")
|
|
|
1524
1545
|
})
|
|
1525
1546
|
.requires("admin");
|
|
1526
1547
|
|
|
1548
|
+
const httpsEndpoint = z.string().url().refine(
|
|
1549
|
+
(value) => value.startsWith("https://"),
|
|
1550
|
+
"capability endpoint must use https",
|
|
1551
|
+
);
|
|
1552
|
+
|
|
1553
|
+
/** Configure where this workspace offers one named capability. */
|
|
1554
|
+
export const configureCapabilityAddress = directive("configureCapabilityAddress")
|
|
1555
|
+
.ensures(CapabilityAddress)
|
|
1556
|
+
.payload(
|
|
1557
|
+
z.object({
|
|
1558
|
+
capability: z.string().regex(/^[a-z0-9][a-z0-9._-]{0,63}$/),
|
|
1559
|
+
endpoint: httpsEndpoint,
|
|
1560
|
+
configuredBy: z.string().min(1),
|
|
1561
|
+
configuredAt: epochMs,
|
|
1562
|
+
}),
|
|
1563
|
+
)
|
|
1564
|
+
.plan((p) => {
|
|
1565
|
+
ensure(CapabilityAddress, capabilityAddressId(p.capability))
|
|
1566
|
+
.set("capability", p.capability)
|
|
1567
|
+
.set("endpoint", p.endpoint)
|
|
1568
|
+
.set("configuredBy", p.configuredBy)
|
|
1569
|
+
.set("configuredAt", p.configuredAt)
|
|
1570
|
+
.set("status", "active");
|
|
1571
|
+
return [];
|
|
1572
|
+
})
|
|
1573
|
+
.requires("admin");
|
|
1574
|
+
|
|
1575
|
+
/** Stop new outbound delivery without deleting the auditable address fact. */
|
|
1576
|
+
export const revokeCapabilityAddress = directive("revokeCapabilityAddress")
|
|
1577
|
+
.mutates(CapabilityAddress)
|
|
1578
|
+
.payload(
|
|
1579
|
+
z.object({
|
|
1580
|
+
addressId: idOf(CapabilityAddress),
|
|
1581
|
+
revokedBy: z.string().min(1),
|
|
1582
|
+
revokedAt: epochMs,
|
|
1583
|
+
}),
|
|
1584
|
+
)
|
|
1585
|
+
.plan((p) => {
|
|
1586
|
+
const address = instance(CapabilityAddress, p.addressId);
|
|
1587
|
+
return [
|
|
1588
|
+
set(address, "configuredBy", p.revokedBy),
|
|
1589
|
+
set(address, "configuredAt", p.revokedAt),
|
|
1590
|
+
set(address, "status", "revoked" as const),
|
|
1591
|
+
];
|
|
1592
|
+
})
|
|
1593
|
+
.requires("admin");
|
|
1594
|
+
|
|
1527
1595
|
/**
|
|
1528
1596
|
* setCloudPolicy — set (or amend) THE QUOTA LAW. An Ensure on the law-state
|
|
1529
1597
|
* singleton: the first author births it, later authors amend it in place —
|
|
@@ -1749,6 +1817,11 @@ export const capabilityBindingByKeyHash = query("capabilityBindingByKeyHash")
|
|
|
1749
1817
|
.key("keyHash")
|
|
1750
1818
|
.returns(CapabilityBinding);
|
|
1751
1819
|
|
|
1820
|
+
/** The current workspace-owned delivery address for a named capability. */
|
|
1821
|
+
export const capabilityAddressByName = query("capabilityAddressByName")
|
|
1822
|
+
.key("capability")
|
|
1823
|
+
.returns(CapabilityAddress);
|
|
1824
|
+
|
|
1752
1825
|
// ── THE ZANZIBAR SPINE comes to the CONTROL PLANE (governance → tuples) ──────────────────────────
|
|
1753
1826
|
// The authority grants above ARE relation tuples in disguise (the same smell as the deleted
|
|
1754
1827
|
// UserPermission): admin=`workspace:self#admin@key:<keyHash>`, creator=`…#creator@key:…`,
|
package/src/idof_lint.ts
CHANGED
|
@@ -140,7 +140,11 @@ export function assertIdOfLintForDirective(dir: Directive<unknown>, where: strin
|
|
|
140
140
|
// this guard a model can compile `entryId: z.string()`, after which every generated caller can mix
|
|
141
141
|
// unrelated record ids and is encouraged to reconstruct identity as application state.
|
|
142
142
|
const effectiveMarker = op.marker ?? (op.aggregateType === dir.aggregateId ? dir.marker : "mutates");
|
|
143
|
-
if (
|
|
143
|
+
if (
|
|
144
|
+
op.aggregateType !== undefined &&
|
|
145
|
+
(effectiveMarker === "mutates" ||
|
|
146
|
+
effectiveMarker === "archives")
|
|
147
|
+
) {
|
|
144
148
|
const payloadField = byValue.get(op.aggregateId);
|
|
145
149
|
if (payloadField !== undefined) {
|
|
146
150
|
const fieldSchema = shape[payloadField];
|
package/src/index.ts
CHANGED
|
@@ -10,11 +10,17 @@
|
|
|
10
10
|
// authors never juggle a separate zod dependency (or risk a second zod instance).
|
|
11
11
|
export { z } from "zod";
|
|
12
12
|
export {
|
|
13
|
+
defineDomain,
|
|
14
|
+
defineFeature,
|
|
15
|
+
featureDependenciesOf,
|
|
13
16
|
definePackage,
|
|
14
17
|
validateNomosPackageConfig,
|
|
18
|
+
type NomosPackageFeatureConfig,
|
|
15
19
|
type NomosPackageConfig,
|
|
16
20
|
type NomosPackageDefinition,
|
|
17
21
|
type NomosPackageDomainConfig,
|
|
22
|
+
type NormalizedNomosPackageConfig,
|
|
23
|
+
type NormalizedNomosPackageDomainConfig,
|
|
18
24
|
} from "./package_config.js";
|
|
19
25
|
export {
|
|
20
26
|
applicationHomeGenesis,
|
|
@@ -50,6 +56,10 @@ export {
|
|
|
50
56
|
type FieldEvolutionDeclaration,
|
|
51
57
|
type StringRules,
|
|
52
58
|
type IntRules,
|
|
59
|
+
type TimestampField,
|
|
60
|
+
type TemporalEnumStates,
|
|
61
|
+
type TemporalEnumFieldDeclaration,
|
|
62
|
+
type TemporalEnumTransition,
|
|
53
63
|
NULL_ADMITTING,
|
|
54
64
|
admitsNull,
|
|
55
65
|
jsonNoNullSchema,
|
|
@@ -113,6 +123,12 @@ export {
|
|
|
113
123
|
type GeoMultiPointValue,
|
|
114
124
|
type ValueObjectUsage,
|
|
115
125
|
} from "./value_object.js";
|
|
126
|
+
export {
|
|
127
|
+
enumType,
|
|
128
|
+
enumTypeNameOf,
|
|
129
|
+
isEnumType,
|
|
130
|
+
type EnumType,
|
|
131
|
+
} from "./enum_type.js";
|
|
116
132
|
export {
|
|
117
133
|
type PrincipalEvidence,
|
|
118
134
|
type AttributionPrincipal,
|
|
@@ -387,6 +403,8 @@ export {
|
|
|
387
403
|
export {
|
|
388
404
|
derived,
|
|
389
405
|
type DerivedDecl,
|
|
406
|
+
type TemporalEnumDecl,
|
|
407
|
+
type AnyDerivedDecl,
|
|
390
408
|
type DerivedOf,
|
|
391
409
|
type TypelessDerived,
|
|
392
410
|
type DeriveFn,
|
|
@@ -407,6 +425,8 @@ export {
|
|
|
407
425
|
EntityGraphBuilder,
|
|
408
426
|
days,
|
|
409
427
|
hours,
|
|
428
|
+
minutes,
|
|
429
|
+
seconds,
|
|
410
430
|
type QueryAst,
|
|
411
431
|
type QueryRow,
|
|
412
432
|
type Report,
|
|
@@ -48,7 +48,9 @@ export type KernelFeature =
|
|
|
48
48
|
/** Law-declared immutable pending-conflict snapshots (compare-and-set merge refs). */
|
|
49
49
|
| "conflictSnapshots"
|
|
50
50
|
/** `.owned()` automatic owner stamp, atomic per-record seat, and framework-custodied epoch-0 key. */
|
|
51
|
-
| "ownedAudience"
|
|
51
|
+
| "ownedAudience"
|
|
52
|
+
/** Captured-clock temporal enum projections declared by `t.timestamp().temporalEnum(...)`. */
|
|
53
|
+
| "temporalEnum";
|
|
52
54
|
|
|
53
55
|
/** The USD-IR era key each derived-from-law capability rides on, for the requirement
|
|
54
56
|
* derivation below (a package that emits the era key needs the capability). */
|
|
@@ -64,6 +66,7 @@ export const ERA_KEY_TO_FEATURE: Readonly<Record<string, KernelFeature>> = {
|
|
|
64
66
|
/** A minimal shape of a compiled domain module — enough to derive its kernel needs. */
|
|
65
67
|
export interface FeatureProbeModule {
|
|
66
68
|
readonly queries?: readonly QueryDecl[];
|
|
69
|
+
readonly deriveds?: readonly unknown[];
|
|
67
70
|
readonly surfacedResolvers?: readonly unknown[];
|
|
68
71
|
readonly aggregates?: readonly { audience?: { kind?: string } }[];
|
|
69
72
|
/** The domain's USD-IR layer (the era keys ride here). Optional — shape-derived
|
|
@@ -83,6 +86,11 @@ export function requiredKernelFeatures(modules: readonly FeatureProbeModule[]):
|
|
|
83
86
|
for (const q of m.queries ?? []) {
|
|
84
87
|
if (Array.isArray(q.key) && q.key.length > 1) needs.add("compoundKeys");
|
|
85
88
|
}
|
|
89
|
+
if ((m.deriveds ?? []).some((derived) =>
|
|
90
|
+
typeof derived === "object" && derived !== null && "kind" in derived &&
|
|
91
|
+
derived.kind === "temporalEnum")) {
|
|
92
|
+
needs.add("temporalEnum");
|
|
93
|
+
}
|
|
86
94
|
if ((m.surfacedResolvers?.length ?? 0) > 0) needs.add("conflictSnapshots");
|
|
87
95
|
if ((m.aggregates ?? []).some((aggregate) => aggregate.audience?.kind === "owned")) {
|
|
88
96
|
needs.add("ownedAudience");
|
|
@@ -251,6 +259,9 @@ export function assessKernelCoherence(params: {
|
|
|
251
259
|
(missing.includes("compoundKeys")
|
|
252
260
|
? `A compound-key query (\`.key(a, b, …)\`) will be refused deep in a law fold with ` +
|
|
253
261
|
`"query declares N index keys; the read router supports exactly one". `
|
|
262
|
+
: missing.includes("temporalEnum")
|
|
263
|
+
? `A temporal enum read will be refused while mounting law with ` +
|
|
264
|
+
`"unknown nomos:readKind temporalEnum". `
|
|
254
265
|
: ``) +
|
|
255
266
|
`Upgrade the cloud's kernel (or point at a cloud whose kernel is coherent with this release).`,
|
|
256
267
|
};
|
package/src/law_check.ts
CHANGED
|
@@ -63,6 +63,12 @@ export interface Finding {
|
|
|
63
63
|
readonly what: string;
|
|
64
64
|
/** How to fix it (or, for a note, how the repair keeps the law valid). */
|
|
65
65
|
readonly fix: string;
|
|
66
|
+
/** Exact authored source anchor when the finding belongs to a file rather than a law declaration. */
|
|
67
|
+
readonly source?: {
|
|
68
|
+
readonly file: string;
|
|
69
|
+
readonly line: number;
|
|
70
|
+
readonly column: number;
|
|
71
|
+
};
|
|
66
72
|
}
|
|
67
73
|
|
|
68
74
|
/** Payload field names the FRAMEWORK consumes off the intent (never the plan) — excluded from the
|
|
@@ -150,6 +156,33 @@ function probeDirective(dir: Directive<unknown>, where: string): ProbeResult | u
|
|
|
150
156
|
return { synth, probePayload, returned, threw, error, ops, reads, readFields };
|
|
151
157
|
}
|
|
152
158
|
|
|
159
|
+
/** Empirical agent-DX guardrail from the CO2 migration: agents model `last…At` observations as a
|
|
160
|
+
* plain integer, erasing clock semantics and then rebuilding freshness in Dart. Keep this narrow:
|
|
161
|
+
* it only warns for the observed `lastXAt`/`lastXTimestamp` shape, and recommends the discoverable
|
|
162
|
+
* timestamp primitive without presuming that every timestamp needs a temporal enum. */
|
|
163
|
+
function timestampFieldFindings(mod: DomainModule): Finding[] {
|
|
164
|
+
const domain = mod.domain ?? mod.name;
|
|
165
|
+
const findings: Finding[] = [];
|
|
166
|
+
for (const aggregate of mod.aggregates) {
|
|
167
|
+
for (const [fieldName, field] of Object.entries(aggregate.fields as Record<string, Field>)) {
|
|
168
|
+
if (
|
|
169
|
+
field.kind === "int" &&
|
|
170
|
+
field.isTimestamp !== true &&
|
|
171
|
+
/^last[A-Z].*(?:At|Timestamp)$/.test(fieldName)
|
|
172
|
+
) {
|
|
173
|
+
findings.push({
|
|
174
|
+
domain,
|
|
175
|
+
directiveId: aggregate.id,
|
|
176
|
+
level: "warn",
|
|
177
|
+
what: `aggregate '${aggregate.id}' field '${fieldName}' looks like a captured timestamp but is declared as a plain t.int(); agents then tend to rebuild age/freshness policy in the client`,
|
|
178
|
+
fix: `declare '${fieldName}: t.timestamp()…'; autocomplete will then expose .temporalEnum(...) when the business value changes with age`,
|
|
179
|
+
});
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return findings;
|
|
184
|
+
}
|
|
185
|
+
|
|
153
186
|
/** `zodObjectShape` with a swallow — a non-object payload yields `{}` (the return twin handles it). */
|
|
154
187
|
function safeShape(schema: z.ZodTypeAny): Record<string, z.ZodTypeAny> {
|
|
155
188
|
try {
|
|
@@ -609,52 +642,97 @@ function isScalarLeafDescriptor(d: ValueObjectFieldDescriptor): boolean {
|
|
|
609
642
|
return d.type === "string" || d.type === "number" || d.type === "boolean" || d.type === "enum" || d.type === "literal";
|
|
610
643
|
}
|
|
611
644
|
|
|
612
|
-
/** Enumerate every dotted leaf path reachable through `object`/`geometry` refs
|
|
613
|
-
* descriptor, `prefix`-rooted.
|
|
645
|
+
/** Enumerate every dotted leaf path reachable through `object`/`geometry` refs or variant arms
|
|
646
|
+
* under one VO descriptor, `prefix`-rooted. Anonymous variant-arm fields are flattened beside
|
|
647
|
+
* `type`; named-VO arms descend through their wire-level `value` member. Arrays/maps are not
|
|
648
|
+
* name-addressable, so they are not descended. */
|
|
614
649
|
function voLeafPaths(
|
|
615
650
|
desc: ValueObjectDescriptor,
|
|
616
651
|
byName: ReadonlyMap<string, ValueObjectDescriptor>,
|
|
617
652
|
prefix: string,
|
|
618
653
|
seen: ReadonlySet<string>,
|
|
619
654
|
): string[] {
|
|
620
|
-
if (desc.fields === undefined) return []; // a geo/union VO exposes no named structural subfields here
|
|
621
655
|
if (seen.has(desc.name)) return []; // cycle guard (a VO transitively nesting itself)
|
|
622
656
|
const nextSeen = new Set(seen).add(desc.name);
|
|
623
|
-
const out
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
628
|
-
|
|
629
|
-
|
|
657
|
+
const out = new Set<string>();
|
|
658
|
+
const visitFields = (
|
|
659
|
+
fields: Readonly<Record<string, ValueObjectFieldDescriptor>>,
|
|
660
|
+
fieldPrefix: string,
|
|
661
|
+
): void => {
|
|
662
|
+
for (const [name, f] of Object.entries(fields)) {
|
|
663
|
+
const path = `${fieldPrefix}.${name}`;
|
|
664
|
+
if (isScalarLeafDescriptor(f)) {
|
|
665
|
+
out.add(path);
|
|
666
|
+
} else if ((f.type === "object" || f.type === "geometry") && byName.has(f.ref)) {
|
|
667
|
+
for (const nested of voLeafPaths(byName.get(f.ref)!, byName, path, nextSeen)) out.add(nested);
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
};
|
|
671
|
+
if (desc.fields !== undefined) visitFields(desc.fields, prefix);
|
|
672
|
+
if (desc.union !== undefined) {
|
|
673
|
+
out.add(`${prefix}.type`);
|
|
674
|
+
for (const arm of Object.values(desc.union)) {
|
|
675
|
+
if ("fields" in arm) {
|
|
676
|
+
visitFields(arm.fields, prefix);
|
|
677
|
+
} else if (byName.has(arm.ref)) {
|
|
678
|
+
for (const nested of voLeafPaths(byName.get(arm.ref)!, byName, `${prefix}.value`, nextSeen)) out.add(nested);
|
|
679
|
+
}
|
|
630
680
|
}
|
|
631
681
|
}
|
|
632
|
-
return out;
|
|
682
|
+
return [...out];
|
|
633
683
|
}
|
|
634
684
|
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
685
|
+
type VoPathVerdict = "leaf" | "nonleaf" | undefined;
|
|
686
|
+
|
|
687
|
+
function resolveFieldPath(
|
|
688
|
+
field: ValueObjectFieldDescriptor,
|
|
689
|
+
byName: ReadonlyMap<string, ValueObjectDescriptor>,
|
|
690
|
+
remaining: readonly string[],
|
|
691
|
+
seen: ReadonlySet<string>,
|
|
692
|
+
): VoPathVerdict {
|
|
693
|
+
if (remaining.length === 0) return isScalarLeafDescriptor(field) ? "leaf" : "nonleaf";
|
|
694
|
+
if ((field.type === "object" || field.type === "geometry") && byName.has(field.ref)) {
|
|
695
|
+
return resolveVoPath(byName.get(field.ref)!, byName, remaining, seen);
|
|
696
|
+
}
|
|
697
|
+
return undefined;
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
/** Resolve one path through a structural VO or a discriminated variant. A field which exists only
|
|
701
|
+
* on SOME anonymous variant arms is still a lawful sparse index: rows in the other arms simply
|
|
702
|
+
* contribute no key, matching the projection's JSON-path extraction semantics. */
|
|
638
703
|
function resolveVoPath(
|
|
639
704
|
rootDesc: ValueObjectDescriptor,
|
|
640
705
|
byName: ReadonlyMap<string, ValueObjectDescriptor>,
|
|
641
706
|
segments: readonly string[],
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
707
|
+
seen: ReadonlySet<string> = new Set(),
|
|
708
|
+
): VoPathVerdict {
|
|
709
|
+
if (segments.length === 0 || seen.has(rootDesc.name)) return undefined;
|
|
710
|
+
const [seg, ...rest] = segments;
|
|
711
|
+
const nextSeen = new Set(seen).add(rootDesc.name);
|
|
712
|
+
|
|
713
|
+
const direct = rootDesc.fields?.[seg!];
|
|
714
|
+
if (direct !== undefined) return resolveFieldPath(direct, byName, rest, nextSeen);
|
|
715
|
+
|
|
716
|
+
if (rootDesc.union === undefined) return undefined;
|
|
717
|
+
if (seg === "type") return rest.length === 0 ? "leaf" : undefined;
|
|
718
|
+
|
|
719
|
+
const verdicts: Exclude<VoPathVerdict, undefined>[] = [];
|
|
720
|
+
for (const arm of Object.values(rootDesc.union)) {
|
|
721
|
+
let verdict: VoPathVerdict;
|
|
722
|
+
if ("fields" in arm) {
|
|
723
|
+
const field = arm.fields[seg!];
|
|
724
|
+
verdict = field === undefined
|
|
725
|
+
? undefined
|
|
726
|
+
: resolveFieldPath(field, byName, rest, nextSeen);
|
|
727
|
+
} else if (seg === "value" && byName.has(arm.ref)) {
|
|
728
|
+
verdict = rest.length === 0
|
|
729
|
+
? "nonleaf"
|
|
730
|
+
: resolveVoPath(byName.get(arm.ref)!, byName, rest, nextSeen);
|
|
655
731
|
}
|
|
732
|
+
if (verdict !== undefined) verdicts.push(verdict);
|
|
656
733
|
}
|
|
657
|
-
return undefined;
|
|
734
|
+
if (verdicts.length === 0) return undefined;
|
|
735
|
+
return verdicts.every((verdict) => verdict === "leaf") ? "leaf" : "nonleaf";
|
|
658
736
|
}
|
|
659
737
|
|
|
660
738
|
function dottedQueryKeyFindings(mod: DomainModule, aggById: ReadonlyMap<string, AggregateHandle>): Finding[] {
|
|
@@ -1239,6 +1317,7 @@ export function checkDomainModules(
|
|
|
1239
1317
|
|
|
1240
1318
|
// Slice-2 twin: the module top level read the captured ports (pristine-pool ineligibility).
|
|
1241
1319
|
findings.push(...topLevelPortReadFindings(mod, portReadingDomains));
|
|
1320
|
+
findings.push(...timestampFieldFindings(mod));
|
|
1242
1321
|
|
|
1243
1322
|
for (const dir of mod.directives as Directive<unknown>[]) {
|
|
1244
1323
|
const where = `${domain}/${dir.id}`;
|