@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
package/src/law_graph.ts
CHANGED
|
@@ -80,7 +80,11 @@ export interface LawFieldPathRef {
|
|
|
80
80
|
* distinguish a nested projection path such as location.point. */
|
|
81
81
|
readonly authoredPath: string;
|
|
82
82
|
/** Stable nested field identities from the aggregate field towards the terminal value. */
|
|
83
|
-
readonly path: readonly (
|
|
83
|
+
readonly path: readonly (
|
|
84
|
+
| LawRef<"field">
|
|
85
|
+
| LawStructuralRef<"value-field">
|
|
86
|
+
| LawStructuralRef<"value-variant">
|
|
87
|
+
)[];
|
|
84
88
|
}
|
|
85
89
|
|
|
86
90
|
export interface LawDomainNode {
|
|
@@ -129,6 +133,8 @@ export interface LawFieldNode {
|
|
|
129
133
|
* sufficient to distinguish e.g. string from JSON, or map<string> from map<json>. */
|
|
130
134
|
readonly storageKind?: FieldKind;
|
|
131
135
|
readonly mapValueKind?: FieldKind;
|
|
136
|
+
/** Semantic scalar identity retained in canonical USDA when the storage kind alone is lossy. */
|
|
137
|
+
readonly valueSemantic?: "epoch-milliseconds";
|
|
132
138
|
/** Content identity of the complete business-value contract used to interpret
|
|
133
139
|
* retained conflict observations. */
|
|
134
140
|
readonly schemaIdentity?: string;
|
|
@@ -377,7 +383,7 @@ export type LawAnonymousValueTypeExpression =
|
|
|
377
383
|
readonly constraints: readonly LawValueConstraint[];
|
|
378
384
|
}
|
|
379
385
|
| { readonly kind: "boolean" | "null" | "unknown" }
|
|
380
|
-
| { readonly kind: "enum"; readonly values: readonly string[] }
|
|
386
|
+
| { readonly kind: "enum"; readonly values: readonly string[]; readonly name?: string }
|
|
381
387
|
| { readonly kind: "literal"; readonly value: LawLiteral }
|
|
382
388
|
| { readonly kind: "list"; readonly element: LawStructuralRef<"value-type"> }
|
|
383
389
|
| { readonly kind: "map"; readonly value: LawStructuralRef<"value-type"> }
|
|
@@ -695,6 +701,13 @@ export interface LawSpatialReadNode extends LawReadBase<"spatial"> {
|
|
|
695
701
|
|
|
696
702
|
export interface LawDerivedReadNode extends LawReadBase<"derived"> {}
|
|
697
703
|
|
|
704
|
+
export interface LawTemporalEnumReadNode extends LawReadBase<"temporalEnum"> {
|
|
705
|
+
readonly source: LawFieldPathRef;
|
|
706
|
+
readonly missing: string;
|
|
707
|
+
readonly initial: string;
|
|
708
|
+
readonly transitions: readonly { readonly afterMillis: number; readonly value: string }[];
|
|
709
|
+
}
|
|
710
|
+
|
|
698
711
|
export interface LawCombinedReadNode extends LawReadBase<"combined"> {
|
|
699
712
|
readonly via: LawFieldPathRef;
|
|
700
713
|
readonly reads: LawRef<"aggregate">;
|
|
@@ -708,6 +721,7 @@ export type LawReadNode =
|
|
|
708
721
|
| LawOrderedReadNode
|
|
709
722
|
| LawSpatialReadNode
|
|
710
723
|
| LawDerivedReadNode
|
|
724
|
+
| LawTemporalEnumReadNode
|
|
711
725
|
| LawCombinedReadNode;
|
|
712
726
|
|
|
713
727
|
export type DeclaredLawNode =
|
|
@@ -815,6 +829,7 @@ export function discoverLawGraph(
|
|
|
815
829
|
owner: aggregateRef,
|
|
816
830
|
value: value.root,
|
|
817
831
|
storageKind: field.kind,
|
|
832
|
+
...(field.isTimestamp === true ? { valueSemantic: "epoch-milliseconds" as const } : {}),
|
|
818
833
|
...(field.mapValueKind === undefined ? {} : { mapValueKind: field.mapValueKind }),
|
|
819
834
|
...(fieldIds.schema === undefined ? {} : { schemaIdentity: fieldIds.schema }),
|
|
820
835
|
evolutions: [...(fieldIds.evolutions ?? [])].sort((left, right) => compareText(left.from, right.from)),
|
|
@@ -894,6 +909,7 @@ export function discoverLawGraph(
|
|
|
894
909
|
owner: contractRef,
|
|
895
910
|
value: value.root,
|
|
896
911
|
storageKind: field.kind,
|
|
912
|
+
...(field.isTimestamp === true ? { valueSemantic: "epoch-milliseconds" as const } : {}),
|
|
897
913
|
...(field.mapValueKind === undefined ? {} : { mapValueKind: field.mapValueKind }),
|
|
898
914
|
evolutions: [],
|
|
899
915
|
mergeDrivers: mergeDrivers(field.driver),
|
|
@@ -2043,7 +2059,14 @@ export function discoverLawGraph(
|
|
|
2043
2059
|
fields: ReadonlyMap<string, LawFieldNode>,
|
|
2044
2060
|
aggregate: string,
|
|
2045
2061
|
path: string,
|
|
2046
|
-
): LawFieldPathRef => fieldPath(
|
|
2062
|
+
): LawFieldPathRef => fieldPath(
|
|
2063
|
+
fields,
|
|
2064
|
+
aggregate,
|
|
2065
|
+
path,
|
|
2066
|
+
fieldNodes,
|
|
2067
|
+
typeNodes,
|
|
2068
|
+
anonymousValueNodes,
|
|
2069
|
+
);
|
|
2047
2070
|
const readNodes = reads.map((read): LawReadNode => {
|
|
2048
2071
|
const readEntity = entity(
|
|
2049
2072
|
registry,
|
|
@@ -2143,6 +2166,15 @@ export function discoverLawGraph(
|
|
|
2143
2166
|
return { ...base, readKind: "spatial", geometry: resolveReadFieldPath(fields, read.of, read.geometry) };
|
|
2144
2167
|
case "derived":
|
|
2145
2168
|
return { ...base, readKind: "derived" };
|
|
2169
|
+
case "temporalEnum":
|
|
2170
|
+
return {
|
|
2171
|
+
...base,
|
|
2172
|
+
readKind: "temporalEnum",
|
|
2173
|
+
source: resolveReadFieldPath(fields, read.of, read.sourceField),
|
|
2174
|
+
missing: read.missing,
|
|
2175
|
+
initial: read.initial,
|
|
2176
|
+
transitions: read.transitions,
|
|
2177
|
+
};
|
|
2146
2178
|
case "combined":
|
|
2147
2179
|
return {
|
|
2148
2180
|
...base,
|
|
@@ -2243,6 +2275,15 @@ type ReadInput =
|
|
|
2243
2275
|
| (ReadInputBase & { readKind: "ordered"; of: string; orderBy: string; descending: boolean; limit: number; where?: CanonicalPred; filtered: boolean })
|
|
2244
2276
|
| (ReadInputBase & { readKind: "spatial"; of: string; geometry: string })
|
|
2245
2277
|
| (ReadInputBase & { readKind: "derived"; of: string; returns: z.ZodTypeAny })
|
|
2278
|
+
| (ReadInputBase & {
|
|
2279
|
+
readKind: "temporalEnum";
|
|
2280
|
+
of: string;
|
|
2281
|
+
sourceField: string;
|
|
2282
|
+
missing: string;
|
|
2283
|
+
initial: string;
|
|
2284
|
+
transitions: readonly { readonly afterMillis: number; readonly value: string }[];
|
|
2285
|
+
returns: z.ZodTypeAny;
|
|
2286
|
+
})
|
|
2246
2287
|
| (ReadInputBase & { readKind: "combined"; of: string; via: string; reads: string; returns: z.ZodTypeAny });
|
|
2247
2288
|
|
|
2248
2289
|
interface RawLawReadParameter {
|
|
@@ -2289,7 +2330,19 @@ function readInputs(module: DomainModule): ReadInput[] {
|
|
|
2289
2330
|
}),
|
|
2290
2331
|
...(module.orderedReads ?? []).map((read) => ({ id: read.id, public: isPublic(read), readKind: "ordered" as const, of: read.of, orderBy: read.orderKey, descending: read.orderDesc, limit: read.limit, ...(read.where === undefined ? {} : { where: read.where }), filtered: read.where !== undefined })),
|
|
2291
2332
|
...(module.spatials ?? []).map((read) => ({ id: read.id, public: isPublic(read), readKind: "spatial" as const, of: read.of, geometry: read.on })),
|
|
2292
|
-
...(module.deriveds ?? []).map((read) =>
|
|
2333
|
+
...(module.deriveds ?? []).map((read) => "kind" in read && read.kind === "temporalEnum"
|
|
2334
|
+
? {
|
|
2335
|
+
id: read.id,
|
|
2336
|
+
public: isPublic(read),
|
|
2337
|
+
readKind: "temporalEnum" as const,
|
|
2338
|
+
of: read.of,
|
|
2339
|
+
sourceField: read.sourceField,
|
|
2340
|
+
missing: read.missing,
|
|
2341
|
+
initial: read.initial,
|
|
2342
|
+
transitions: read.transitions,
|
|
2343
|
+
returns: read.returns,
|
|
2344
|
+
}
|
|
2345
|
+
: { id: read.id, public: isPublic(read), readKind: "derived" as const, of: read.of, returns: read.returns }),
|
|
2293
2346
|
...(module.combineds ?? []).map((read) => ({ id: read.id, public: isPublic(read), readKind: "combined" as const, of: read.of, via: read.refField, reads: read.reads, returns: read.returns })),
|
|
2294
2347
|
];
|
|
2295
2348
|
return reads.sort((a, b) => compareText(a.id, b.id));
|
|
@@ -2359,6 +2412,7 @@ function readResult(read: ReadInput, aggregate: LawRef<"aggregate">): RawLawRead
|
|
|
2359
2412
|
value: { kind: "nullable", value: { kind: "integer", constraints: [] } },
|
|
2360
2413
|
};
|
|
2361
2414
|
case "derived":
|
|
2415
|
+
case "temporalEnum":
|
|
2362
2416
|
case "combined":
|
|
2363
2417
|
return { kind: "scalar", value: lowerLawValueType(read.returns) };
|
|
2364
2418
|
}
|
|
@@ -2457,7 +2511,11 @@ function descriptorValue(field: ValueObjectFieldDescriptor): LawValueType {
|
|
|
2457
2511
|
case "boolean":
|
|
2458
2512
|
return { kind: "boolean" };
|
|
2459
2513
|
case "enum":
|
|
2460
|
-
return {
|
|
2514
|
+
return {
|
|
2515
|
+
kind: "enum",
|
|
2516
|
+
values: [...field.values].sort(compareText),
|
|
2517
|
+
...(field.name === undefined ? {} : { name: field.name }),
|
|
2518
|
+
};
|
|
2461
2519
|
case "literal":
|
|
2462
2520
|
return { kind: "literal", value: field.value };
|
|
2463
2521
|
case "array":
|
|
@@ -2577,7 +2635,11 @@ export function lowerAnonymousValueGraph<K extends LawEntityKind>(
|
|
|
2577
2635
|
expression = { kind: value.kind };
|
|
2578
2636
|
break;
|
|
2579
2637
|
case "enum":
|
|
2580
|
-
expression = {
|
|
2638
|
+
expression = {
|
|
2639
|
+
kind: "enum",
|
|
2640
|
+
values: value.values,
|
|
2641
|
+
...(value.name === undefined ? {} : { name: value.name }),
|
|
2642
|
+
};
|
|
2581
2643
|
break;
|
|
2582
2644
|
case "literal":
|
|
2583
2645
|
expression = { kind: "literal", value: value.value };
|
|
@@ -2941,12 +3003,17 @@ function fieldPath(
|
|
|
2941
3003
|
aggregate: string,
|
|
2942
3004
|
path: string,
|
|
2943
3005
|
allFields: readonly LawFieldNode[],
|
|
3006
|
+
allTypes: readonly LawTypeNode[],
|
|
2944
3007
|
anonymousNodes: ReadonlyMap<string, LawAnonymousValueNode>,
|
|
2945
3008
|
): LawFieldPathRef {
|
|
2946
3009
|
const [head, ...tail] = path.split(".");
|
|
2947
3010
|
const field = head === undefined ? undefined : fields.get(head);
|
|
2948
3011
|
if (field === undefined) fail(`read field path '${aggregate}.${path}' targets an unknown field`);
|
|
2949
|
-
const nested: Array<
|
|
3012
|
+
const nested: Array<
|
|
3013
|
+
| LawRef<"field">
|
|
3014
|
+
| LawStructuralRef<"value-field">
|
|
3015
|
+
| LawStructuralRef<"value-variant">
|
|
3016
|
+
> = [];
|
|
2950
3017
|
let value: LawValueTypeRef = field.value;
|
|
2951
3018
|
const unwrap = (): void => {
|
|
2952
3019
|
const seen = new Set<string>();
|
|
@@ -2965,21 +3032,85 @@ function fieldPath(
|
|
|
2965
3032
|
};
|
|
2966
3033
|
for (const segment of tail) {
|
|
2967
3034
|
unwrap();
|
|
2968
|
-
const
|
|
2969
|
-
|
|
2970
|
-
|
|
2971
|
-
|
|
2972
|
-
|
|
3035
|
+
const fieldAt = (
|
|
3036
|
+
owner: LawValueTypeRef,
|
|
3037
|
+
): { ref: LawRef<"field"> | LawStructuralRef<"value-field">; value: LawValueTypeRef } | undefined => {
|
|
3038
|
+
const declared = allFields.find((candidate) =>
|
|
3039
|
+
candidate.owner.id === owner.id && candidate.entity.label === segment);
|
|
3040
|
+
if (declared !== undefined) return { ref: ref(declared.entity), value: declared.value };
|
|
3041
|
+
const structural = [...anonymousNodes.values()].find(
|
|
3042
|
+
(candidate): candidate is LawAnonymousValueFieldNode =>
|
|
3043
|
+
candidate.structuralKind === "value-field" &&
|
|
3044
|
+
candidate.owner.id === owner.id &&
|
|
3045
|
+
candidate.name === segment,
|
|
3046
|
+
);
|
|
3047
|
+
return structural === undefined
|
|
3048
|
+
? undefined
|
|
3049
|
+
: { ref: structuralRef(structural.entity), value: structural.value };
|
|
3050
|
+
};
|
|
3051
|
+
|
|
3052
|
+
const direct = fieldAt(value);
|
|
3053
|
+
if (direct !== undefined) {
|
|
3054
|
+
nested.push(direct.ref);
|
|
3055
|
+
value = direct.value;
|
|
2973
3056
|
continue;
|
|
2974
3057
|
}
|
|
2975
|
-
|
|
2976
|
-
|
|
2977
|
-
|
|
2978
|
-
|
|
2979
|
-
|
|
2980
|
-
|
|
2981
|
-
|
|
2982
|
-
|
|
3058
|
+
|
|
3059
|
+
// A `variant(...)` lowers to an anonymous SUM of RECORD arms. Runtime dotted indexes are
|
|
3060
|
+
// intentionally sparse: `scope.buildingId` exists only on the buildingLevel arm and the other
|
|
3061
|
+
// arms contribute no row. Walk the variant branches here too, so the canonical USDA graph and
|
|
3062
|
+
// the projection manifest agree instead of forcing authors back to nullable top-level ids.
|
|
3063
|
+
const valueNode = anonymousNodes.get(value.id);
|
|
3064
|
+
const variants = valueNode?.structuralKind === "value-type" && valueNode.expression.kind === "sum"
|
|
3065
|
+
? valueNode.expression.variants
|
|
3066
|
+
: allTypes.find((candidate) => candidate.entity.id === value.id && candidate.form === "sum")?.variants;
|
|
3067
|
+
if (variants !== undefined) {
|
|
3068
|
+
if (segment === "type") {
|
|
3069
|
+
const first = [...variants].sort((a, b) => compareText(a.id, b.id))[0];
|
|
3070
|
+
const variant = first === undefined ? undefined : anonymousNodes.get(first.id);
|
|
3071
|
+
if (variant?.structuralKind === "value-variant") {
|
|
3072
|
+
// The discriminator is represented by the variant identity itself in canonical USDA;
|
|
3073
|
+
// it is not duplicated as a synthetic string field on every arm.
|
|
3074
|
+
nested.push(structuralRef(variant.entity));
|
|
3075
|
+
value = variant.value;
|
|
3076
|
+
continue;
|
|
3077
|
+
}
|
|
3078
|
+
}
|
|
3079
|
+
const matches: Array<{
|
|
3080
|
+
variant: LawAnonymousValueVariantNode;
|
|
3081
|
+
field: LawRef<"field"> | LawStructuralRef<"value-field">;
|
|
3082
|
+
value: LawValueTypeRef;
|
|
3083
|
+
}> = [];
|
|
3084
|
+
for (const variantRef of variants) {
|
|
3085
|
+
const variant = anonymousNodes.get(variantRef.id);
|
|
3086
|
+
if (variant?.structuralKind !== "value-variant") continue;
|
|
3087
|
+
let armValue = variant.value;
|
|
3088
|
+
const armSeen = new Set<string>();
|
|
3089
|
+
while (true) {
|
|
3090
|
+
if (armSeen.has(armValue.id)) fail(`read field path '${aggregate}.${path}' crosses a variant wrapper cycle`);
|
|
3091
|
+
armSeen.add(armValue.id);
|
|
3092
|
+
const armNode = anonymousNodes.get(armValue.id);
|
|
3093
|
+
if (
|
|
3094
|
+
armNode?.structuralKind !== "value-type" ||
|
|
3095
|
+
(armNode.expression.kind !== "nullable" &&
|
|
3096
|
+
armNode.expression.kind !== "default" &&
|
|
3097
|
+
armNode.expression.kind !== "recursive")
|
|
3098
|
+
) break;
|
|
3099
|
+
armValue = armNode.expression.value;
|
|
3100
|
+
}
|
|
3101
|
+
const match = fieldAt(armValue);
|
|
3102
|
+
if (match !== undefined) matches.push({ variant, field: match.ref, value: match.value });
|
|
3103
|
+
}
|
|
3104
|
+
if (matches.length > 0) {
|
|
3105
|
+
// Variant options are canonicalised before lowering; sorting again makes the selected
|
|
3106
|
+
// structural witness explicit and deterministic when a common field (notably `type`)
|
|
3107
|
+
// occurs on every arm. `authoredPath` remains the executable path.
|
|
3108
|
+
matches.sort((a, b) => compareText(a.variant.entity.id, b.variant.entity.id));
|
|
3109
|
+
const match = matches[0]!;
|
|
3110
|
+
nested.push(structuralRef(match.variant.entity), match.field);
|
|
3111
|
+
value = match.value;
|
|
3112
|
+
continue;
|
|
3113
|
+
}
|
|
2983
3114
|
}
|
|
2984
3115
|
fail(`read field path '${aggregate}.${path}' targets unknown nested field '${segment}'`);
|
|
2985
3116
|
}
|
package/src/law_graph_usda.ts
CHANGED
|
@@ -1158,6 +1158,23 @@ function emitRead(
|
|
|
1158
1158
|
break;
|
|
1159
1159
|
case "derived":
|
|
1160
1160
|
break;
|
|
1161
|
+
case "temporalEnum":
|
|
1162
|
+
body.push(`${pad}custom rel nomos:source = ${fieldTarget(read.source)}`);
|
|
1163
|
+
body.push(`${pad}custom uniform string nomos:sourcePath = ${quote(read.source.authoredPath)}`);
|
|
1164
|
+
body.push(`${pad}custom uniform token nomos:missing = ${quote(read.missing)}`);
|
|
1165
|
+
body.push(`${pad}custom uniform token nomos:initial = ${quote(read.initial)}`);
|
|
1166
|
+
body.push("", `${pad}def Scope "Transitions"`, `${pad}{`);
|
|
1167
|
+
for (const [index, transition] of read.transitions.entries()) {
|
|
1168
|
+
body.push(
|
|
1169
|
+
`${pad} def NomosTemporalTransition "After_${String(index).padStart(4, "0")}"`,
|
|
1170
|
+
`${pad} {`,
|
|
1171
|
+
`${pad} custom uniform int64 nomos:afterMillis = ${transition.afterMillis}`,
|
|
1172
|
+
`${pad} custom uniform token nomos:value = ${quote(transition.value)}`,
|
|
1173
|
+
`${pad} }`,
|
|
1174
|
+
);
|
|
1175
|
+
}
|
|
1176
|
+
body.push(`${pad}}`);
|
|
1177
|
+
break;
|
|
1161
1178
|
case "combined":
|
|
1162
1179
|
body.push(`${pad}custom rel nomos:refField = ${fieldTarget(read.via)}`);
|
|
1163
1180
|
body.push(`${pad}custom uniform string nomos:refFieldPath = ${quote(read.via.authoredPath)}`);
|
|
@@ -1484,6 +1501,9 @@ function emitAnonymousType(
|
|
|
1484
1501
|
case "enum":
|
|
1485
1502
|
body.push(`${pad}custom uniform token nomos:typeKind = "scalar"`);
|
|
1486
1503
|
body.push(`${pad}custom uniform token nomos:scalar = "string"`);
|
|
1504
|
+
if (expression.name !== undefined) {
|
|
1505
|
+
body.push(`${pad}custom uniform string nomos:declaredName = ${quote(expression.name)}`);
|
|
1506
|
+
}
|
|
1487
1507
|
body.push(`${pad}custom uniform token[] nomos:enum = [${expression.values.map(quote).join(", ")}]`);
|
|
1488
1508
|
break;
|
|
1489
1509
|
case "literal":
|
|
@@ -1674,6 +1694,9 @@ function emitField(
|
|
|
1674
1694
|
if (field.mapValueKind !== undefined) {
|
|
1675
1695
|
body.push(`${pad} custom uniform token nomos:mapValueKind = ${quote(field.mapValueKind)}`);
|
|
1676
1696
|
}
|
|
1697
|
+
if (field.valueSemantic !== undefined) {
|
|
1698
|
+
body.push(`${pad} custom uniform token nomos:valueSemantic = ${quote(field.valueSemantic)}`);
|
|
1699
|
+
}
|
|
1677
1700
|
if (field.schemaIdentity !== undefined) {
|
|
1678
1701
|
body.push(`${pad} custom uniform string nomos:schemaIdentity = ${quote(field.schemaIdentity)}`);
|
|
1679
1702
|
}
|
package/src/law_value_type.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import type { z } from "zod";
|
|
2
|
+
import { enumTypeNameOf } from "./enum_type.js";
|
|
2
3
|
import {
|
|
3
4
|
zodArrayElement,
|
|
4
5
|
zodDef,
|
|
@@ -26,7 +27,7 @@ export interface LawValueMember {
|
|
|
26
27
|
export type LawValueType =
|
|
27
28
|
| { readonly kind: "string" | "number" | "integer"; readonly constraints: readonly LawValueConstraint[] }
|
|
28
29
|
| { readonly kind: "boolean" | "null" | "unknown" }
|
|
29
|
-
| { readonly kind: "enum"; readonly values: readonly string[] }
|
|
30
|
+
| { readonly kind: "enum"; readonly values: readonly string[]; readonly name?: string }
|
|
30
31
|
| { readonly kind: "literal"; readonly value: LawLiteral }
|
|
31
32
|
| { readonly kind: "list"; readonly element: LawValueType }
|
|
32
33
|
| { readonly kind: "map"; readonly value: LawValueType }
|
|
@@ -91,8 +92,14 @@ function lower(
|
|
|
91
92
|
case "unknown":
|
|
92
93
|
case "any":
|
|
93
94
|
return { kind: "unknown" };
|
|
94
|
-
case "enum":
|
|
95
|
-
|
|
95
|
+
case "enum": {
|
|
96
|
+
const name = enumTypeNameOf(schema);
|
|
97
|
+
return {
|
|
98
|
+
kind: "enum",
|
|
99
|
+
values: zodEnumValues(schema).sort(compareText),
|
|
100
|
+
...(name === undefined ? {} : { name }),
|
|
101
|
+
};
|
|
102
|
+
}
|
|
96
103
|
case "literal":
|
|
97
104
|
return { kind: "literal", value: literalOf(def, path) };
|
|
98
105
|
case "array":
|
package/src/lsp_server.ts
CHANGED
|
@@ -23,6 +23,7 @@
|
|
|
23
23
|
import { existsSync, readdirSync, readFileSync, statSync } from "node:fs";
|
|
24
24
|
import path from "node:path";
|
|
25
25
|
import { pathToFileURL, fileURLToPath } from "node:url";
|
|
26
|
+
import { bindEnumTypeExports } from "./enum_type.js";
|
|
26
27
|
|
|
27
28
|
import {
|
|
28
29
|
createConnection,
|
|
@@ -52,17 +53,18 @@ import {
|
|
|
52
53
|
import { parseStableIdLockfile } from "./stable_id_lockfile.js";
|
|
53
54
|
import {
|
|
54
55
|
validateNomosPackageConfig,
|
|
55
|
-
type
|
|
56
|
+
type NormalizedNomosPackageConfig,
|
|
57
|
+
type NormalizedNomosPackageDomainConfig,
|
|
56
58
|
} from "./package_config.js";
|
|
57
59
|
import { assertPublicPackageImports } from "./package_source_imports.js";
|
|
60
|
+
import {
|
|
61
|
+
assertPackageFeatureTopology,
|
|
62
|
+
packageFeatureTopologyFindings,
|
|
63
|
+
} from "./package_feature_topology.js";
|
|
58
64
|
import { packageDependencyUsageFindings } from "./package_dependency_usage.js";
|
|
59
65
|
|
|
60
|
-
interface DomainConfig {
|
|
61
|
-
|
|
62
|
-
name?: string;
|
|
63
|
-
modules: string[];
|
|
64
|
-
}
|
|
65
|
-
interface PackageConfig extends NomosPackageConfig {
|
|
66
|
+
interface DomainConfig extends NormalizedNomosPackageDomainConfig {}
|
|
67
|
+
interface PackageConfig extends NormalizedNomosPackageConfig {
|
|
66
68
|
readonly domains: readonly DomainConfig[];
|
|
67
69
|
}
|
|
68
70
|
|
|
@@ -137,7 +139,6 @@ async function composePackage(
|
|
|
137
139
|
};
|
|
138
140
|
const validated = validateNomosPackageConfig(cfgModule.default);
|
|
139
141
|
const cfg = validated.config as PackageConfig;
|
|
140
|
-
|
|
141
142
|
// Resolve every module file first so we can compute the mtime cache key before importing.
|
|
142
143
|
const perDomainFiles: { key: string; name: string | undefined; files: string[] }[] = [];
|
|
143
144
|
const allFiles: string[] = [];
|
|
@@ -162,6 +163,10 @@ async function composePackage(
|
|
|
162
163
|
};
|
|
163
164
|
}
|
|
164
165
|
|
|
166
|
+
const sourceTopologyFindings = packageFeatureTopologyFindings(
|
|
167
|
+
assertPackageFeatureTopology(cfg.domains, cfgDir),
|
|
168
|
+
cfgDir,
|
|
169
|
+
);
|
|
165
170
|
assertPublicPackageImports(allFiles);
|
|
166
171
|
|
|
167
172
|
const composed: DomainModule[] = [];
|
|
@@ -169,17 +174,22 @@ async function composePackage(
|
|
|
169
174
|
for (const d of perDomainFiles) {
|
|
170
175
|
const mods: Mod[] = [];
|
|
171
176
|
for (const abs of d.files) {
|
|
172
|
-
|
|
177
|
+
const mod = (await import(`${pathToFileURL(abs).href}?t=${Date.now()}`)) as Mod;
|
|
178
|
+
bindEnumTypeExports(mod, abs);
|
|
179
|
+
mods.push(mod);
|
|
173
180
|
}
|
|
174
181
|
domainFiles.set(d.key, d.files);
|
|
175
182
|
composed.push(composeDomainModule({ name: d.name ?? d.key, domain: d.key, modules: mods }));
|
|
176
183
|
}
|
|
177
|
-
const dependencyFindings =
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
184
|
+
const dependencyFindings = [
|
|
185
|
+
...sourceTopologyFindings,
|
|
186
|
+
...packageDependencyUsageFindings({
|
|
187
|
+
packageName: cfg.name,
|
|
188
|
+
exportedDomains: cfg.domains.map((domain) => domain.key),
|
|
189
|
+
runtimeDependencies: validated.runtimeDependencies,
|
|
190
|
+
modules: composed,
|
|
191
|
+
}),
|
|
192
|
+
];
|
|
183
193
|
composedCache.set(configPath, { mtimeKey: key, modules: composed, domainFiles, dependencyFindings });
|
|
184
194
|
return { modules: composed, domainFiles, dependencyFindings };
|
|
185
195
|
}
|
|
@@ -269,7 +279,7 @@ export async function runCheckPass(configPath: string, against?: string): Promis
|
|
|
269
279
|
|
|
270
280
|
const srcMap = new LawSourceMap(domainFiles);
|
|
271
281
|
const located: LocatedFinding[] = findings.map((f) => {
|
|
272
|
-
const loc = srcMap.locate(f.domain, f.directiveId);
|
|
282
|
+
const loc = f.source ?? srcMap.locate(f.domain, f.directiveId);
|
|
273
283
|
return {
|
|
274
284
|
finding: f,
|
|
275
285
|
file: loc?.file ?? configPath,
|
package/src/package_config.ts
CHANGED
|
@@ -9,13 +9,57 @@ import {
|
|
|
9
9
|
type ApplicationHomeGenesis,
|
|
10
10
|
} from "./application_home_genesis.js";
|
|
11
11
|
|
|
12
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* One cohesive, vertically sliced business capability inside a domain.
|
|
14
|
+
*
|
|
15
|
+
* Features are values rather than loose names so dependencies are selected from declarations which
|
|
16
|
+
* actually exist. Source paths remain paths (that is their type), but the business topology does not
|
|
17
|
+
* regress into an unrelated string graph.
|
|
18
|
+
*/
|
|
19
|
+
export interface NomosPackageFeatureConfig<Key extends string = string> {
|
|
20
|
+
readonly key: Key;
|
|
21
|
+
readonly modules: readonly string[];
|
|
22
|
+
/** A thunk permits honest bidirectional domain relationships without string-key dependency graphs. */
|
|
23
|
+
readonly dependsOn?: readonly NomosPackageFeatureConfig[] | (() => readonly NomosPackageFeatureConfig[]);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export function featureDependenciesOf(
|
|
27
|
+
feature: NomosPackageFeatureConfig,
|
|
28
|
+
): readonly NomosPackageFeatureConfig[] {
|
|
29
|
+
return typeof feature.dependsOn === "function" ? feature.dependsOn() : feature.dependsOn ?? [];
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/** Define a typed feature value which may be used directly by dependent features. */
|
|
33
|
+
export function defineFeature<const Config extends NomosPackageFeatureConfig>(
|
|
34
|
+
config: Config,
|
|
35
|
+
): Config {
|
|
36
|
+
return config;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** The named feature composition which forms one domain exported by a Nomos package. */
|
|
13
40
|
export interface NomosPackageDomainConfig {
|
|
14
41
|
readonly key: string;
|
|
15
42
|
readonly name?: string;
|
|
43
|
+
readonly features: readonly NomosPackageFeatureConfig[];
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/** Define a domain composition root. Domain roots own topology only, never business declarations. */
|
|
47
|
+
export function defineDomain<const Config extends NomosPackageDomainConfig>(
|
|
48
|
+
config: Config,
|
|
49
|
+
): Config {
|
|
50
|
+
return config;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Compiler-normalized domain. Raw modules are deliberately absent from the public authoring API. */
|
|
54
|
+
export interface NormalizedNomosPackageDomainConfig extends NomosPackageDomainConfig {
|
|
16
55
|
readonly modules: readonly string[];
|
|
17
56
|
}
|
|
18
57
|
|
|
58
|
+
/** The checked package shape consumed by compiler/check/LSP internals. */
|
|
59
|
+
export interface NormalizedNomosPackageConfig extends Omit<NomosPackageConfig, "domains"> {
|
|
60
|
+
readonly domains: readonly NormalizedNomosPackageDomainConfig[];
|
|
61
|
+
}
|
|
62
|
+
|
|
19
63
|
/**
|
|
20
64
|
* The common, law-bearing part of `nomos.project.ts`.
|
|
21
65
|
*
|
|
@@ -74,7 +118,7 @@ export type NomosPackageDefinition<Config extends NomosPackageConfig> = Config &
|
|
|
74
118
|
|
|
75
119
|
/** Validate the common package contract and return its canonical runtime requirements. */
|
|
76
120
|
export function validateNomosPackageConfig(value: unknown): {
|
|
77
|
-
readonly config:
|
|
121
|
+
readonly config: NormalizedNomosPackageConfig;
|
|
78
122
|
readonly runtimeDependencies: readonly RuntimePackageDependency[];
|
|
79
123
|
} {
|
|
80
124
|
if (value === null || typeof value !== "object") {
|
|
@@ -85,24 +129,80 @@ export function validateNomosPackageConfig(value: unknown): {
|
|
|
85
129
|
throw new Error("config needs a non-empty string name");
|
|
86
130
|
}
|
|
87
131
|
if (!Array.isArray(config.domains) || config.domains.length === 0) {
|
|
88
|
-
throw new Error("config needs domains: [{ key,
|
|
132
|
+
throw new Error("config needs domains: [{ key, features }, ...]");
|
|
89
133
|
}
|
|
90
134
|
const domainKeys = new Set<string>();
|
|
91
|
-
|
|
135
|
+
const normalizedDomains: NormalizedNomosPackageDomainConfig[] = [];
|
|
136
|
+
for (const candidate of config.domains) {
|
|
137
|
+
const domain = candidate as NomosPackageDomainConfig;
|
|
92
138
|
if (
|
|
93
139
|
domain === null
|
|
94
140
|
|| typeof domain !== "object"
|
|
95
141
|
|| typeof domain.key !== "string"
|
|
96
142
|
|| domain.key.length === 0
|
|
97
|
-
|| !Array.isArray(domain.
|
|
98
|
-
|| domain.
|
|
99
|
-
|| domain.modules.some((module: unknown) => typeof module !== "string" || module.length === 0)
|
|
143
|
+
|| !Array.isArray(domain.features)
|
|
144
|
+
|| domain.features.length === 0
|
|
100
145
|
) {
|
|
101
|
-
throw new Error(`each domain needs { key,
|
|
146
|
+
throw new Error(`each domain needs { key, features: [defineFeature(...), ...] }; got ${JSON.stringify(domain)}`);
|
|
102
147
|
}
|
|
103
148
|
if (!domainKeys.add(domain.key)) {
|
|
104
149
|
throw new Error(`package '${config.name}' repeats domain key '${domain.key}'`);
|
|
105
150
|
}
|
|
151
|
+
const featureKeys = new Set<string>();
|
|
152
|
+
const featureObjects = new Set<NomosPackageFeatureConfig>();
|
|
153
|
+
const moduleOwners = new Map<string, string>();
|
|
154
|
+
for (const feature of domain.features) {
|
|
155
|
+
if (
|
|
156
|
+
feature === null
|
|
157
|
+
|| typeof feature !== "object"
|
|
158
|
+
|| typeof feature.key !== "string"
|
|
159
|
+
|| feature.key.length === 0
|
|
160
|
+
|| !Array.isArray(feature.modules)
|
|
161
|
+
|| feature.modules.length === 0
|
|
162
|
+
|| feature.modules.some((module: unknown) => typeof module !== "string" || module.length === 0)
|
|
163
|
+
) {
|
|
164
|
+
throw new Error(
|
|
165
|
+
`domain '${domain.key}' features need { key, modules: [path, ...] }; got ${JSON.stringify(feature)}`,
|
|
166
|
+
);
|
|
167
|
+
}
|
|
168
|
+
if (!featureKeys.add(feature.key)) {
|
|
169
|
+
throw new Error(`domain '${domain.key}' repeats feature key '${feature.key}'`);
|
|
170
|
+
}
|
|
171
|
+
featureObjects.add(feature);
|
|
172
|
+
for (const module of feature.modules) {
|
|
173
|
+
const prior = moduleOwners.get(module);
|
|
174
|
+
if (prior !== undefined) {
|
|
175
|
+
throw new Error(
|
|
176
|
+
`domain '${domain.key}' module '${module}' belongs to both '${prior}' and '${feature.key}'; ` +
|
|
177
|
+
"every law source must have exactly one feature owner",
|
|
178
|
+
);
|
|
179
|
+
}
|
|
180
|
+
moduleOwners.set(module, feature.key);
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
for (const feature of domain.features) {
|
|
184
|
+
if (
|
|
185
|
+
feature.dependsOn !== undefined
|
|
186
|
+
&& !Array.isArray(feature.dependsOn)
|
|
187
|
+
&& typeof feature.dependsOn !== "function"
|
|
188
|
+
) {
|
|
189
|
+
throw new Error(`domain '${domain.key}' feature '${feature.key}' dependsOn must be an array or thunk`);
|
|
190
|
+
}
|
|
191
|
+
for (const dependency of featureDependenciesOf(feature)) {
|
|
192
|
+
if (!featureObjects.has(dependency)) {
|
|
193
|
+
throw new Error(
|
|
194
|
+
`domain '${domain.key}' feature '${feature.key}' depends on a feature value outside its domain`,
|
|
195
|
+
);
|
|
196
|
+
}
|
|
197
|
+
if (dependency === feature) {
|
|
198
|
+
throw new Error(`domain '${domain.key}' feature '${feature.key}' cannot depend on itself`);
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
normalizedDomains.push({
|
|
203
|
+
...domain,
|
|
204
|
+
modules: domain.features.flatMap((feature) => feature.modules),
|
|
205
|
+
});
|
|
106
206
|
}
|
|
107
207
|
if (config.dependencies !== undefined && !Array.isArray(config.dependencies)) {
|
|
108
208
|
throw new Error("package dependencies must be an array");
|
|
@@ -130,7 +230,10 @@ export function validateNomosPackageConfig(value: unknown): {
|
|
|
130
230
|
config.dependencies ?? [],
|
|
131
231
|
{ requiresNomos: true },
|
|
132
232
|
);
|
|
133
|
-
return {
|
|
233
|
+
return {
|
|
234
|
+
config: { ...config, domains: normalizedDomains } as NormalizedNomosPackageConfig,
|
|
235
|
+
runtimeDependencies,
|
|
236
|
+
};
|
|
134
237
|
}
|
|
135
238
|
|
|
136
239
|
/**
|