@githolon/dsl 0.102.5 → 0.103.1
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/compile_package.mjs +43 -2
- package/compile_proof.d.mts +47 -0
- package/compile_proof.mjs +343 -0
- package/dart/CHANGELOG.md +14 -0
- package/dart/lib/src/dev_cloud.dart +8 -8
- package/dart/lib/src/nms_generated_runtime.dart +2 -0
- package/dart/lib/src/runner/node-entry.mjs +5539 -1228
- package/dart/lib/src/runner/offline-install.mjs +4578 -528
- package/dart/lib/src/test_harness.dart +206 -152
- package/dart/lib/src/webview_bridge.dart +239 -102
- package/dart/pubspec.lock +1 -1
- package/dart/pubspec.yaml +2 -2
- package/dart/test/acquire_session_credentials_test.dart +4 -3
- package/dart/test/acquire_session_test.dart +9 -0
- package/dart/test/adv_realm_interference_test.dart +7 -6
- package/dart/test/cloud_bound_topology_test.dart +3 -3
- package/dart/test/cluster_births_test.dart +4 -3
- package/dart/test/cluster_device_keyed_acceptance_test.dart +10 -16
- package/dart/test/cluster_local_autosync_test.dart +3 -2
- package/dart/test/cluster_scale_test.dart +3 -2
- package/dart/test/dlq_session_surface_test.dart +7 -6
- package/dart/test/first_app_gauntlet_test.dart +14 -21
- package/dart/test/fixtures/wedge/fake-install.mjs +1 -1
- package/dart/test/four_layer_nested_topology_test.dart +4 -4
- package/dart/test/harness_phase_timeout_test.dart +2 -2
- package/dart/test/local_sync_inert_test.dart +1 -1
- package/dart/test/parent_signer_recovery_test.dart +2 -2
- package/dart/test/sealing_client_test.dart +4 -3
- package/dart/test/session_bound_test.dart +8 -7
- package/dart/test/test_harness_test.dart +13 -12
- package/dart/test/vo_read_typing_test.dart +4 -3
- package/dart/test/watch_after_offer_stall_test.dart +4 -3
- package/dart/test/webview_bridge_test.dart +102 -5
- package/dart_types/CHANGELOG.md +5 -0
- package/dart_types/lib/nomos_types.dart +9 -0
- package/dart_types/lib/src/nomos_ref.dart +20 -0
- package/dart_types/lib/src/optional_ref.dart +85 -0
- package/dart_types/lib/src/sha256_digest.dart +41 -0
- package/dart_types/lib/src/versioned_nomos_ref.dart +81 -0
- package/dart_types/pubspec.yaml +1 -1
- package/dart_types/test/optional_ref_test.dart +44 -0
- package/dart_types/test/sha256_digest_test.dart +18 -0
- package/package.json +9 -5
- package/runtime/generated-application-host.js +5365 -1105
- package/src/aggregate.ts +18 -0
- package/src/application_composition.ts +6 -32
- package/src/attested_core.ts +6 -3
- package/src/birth_meter.ts +1 -0
- package/src/birth_meter_ensure.ts +28 -2
- package/src/births.ts +1 -1
- package/src/build_package.ts +162 -538
- package/src/business_birth.ts +19 -3
- package/src/canonical_json.ts +21 -0
- package/src/check_package_main.ts +73 -33
- package/src/clear_directive.ts +19 -8
- package/src/codegen_dart.ts +305 -22
- package/src/codegen_dart_application.ts +862 -292
- package/src/codegen_dart_client.ts +1084 -407
- package/src/codegen_dart_scenario.ts +12 -12
- package/src/codegen_proof.ts +169 -39
- package/src/codegen_provider_dart.ts +17 -1
- package/src/codegen_ts.ts +298 -106
- package/src/compat_manifest.ts +2 -2
- package/src/compat_package_main.ts +8 -12
- package/src/compile_package_main.ts +1860 -592
- package/src/compose.ts +1 -1
- package/src/directive.ts +208 -15
- package/src/engine_entry.ts +165 -111
- package/src/evolve_preview.ts +29 -69
- package/src/extremum.ts +1 -1
- package/src/fields.ts +183 -4
- package/src/flow_gate.ts +75 -2
- package/src/framework/application_home.ts +2 -0
- package/src/framework/auth_provider_law.ts +2 -2
- package/src/framework/birth_meter.ts +7 -19
- package/src/framework/bootstrap.ts +0 -2
- package/src/framework/business_corrections.ts +9 -5
- package/src/framework/domain_lifecycle.ts +15 -29
- package/src/framework/encryption_scope.ts +2 -1
- package/src/framework/framework_aggregates.ts +2 -0
- package/src/framework/genesis.ts +16 -14
- package/src/framework/home.ts +20 -14
- package/src/framework/owned_custody.ts +9 -3
- package/src/framework/platform_admin.ts +3 -3
- package/src/framework/relations.ts +21 -13
- package/src/framework/signing_identity.ts +45 -40
- package/src/framework/workspace_invariant.ts +245 -0
- package/src/framework/workspaces.ts +23 -29
- package/src/index.ts +36 -16
- package/src/intent.ts +178 -6
- package/src/law_check.ts +32 -11
- package/src/law_graph.ts +2902 -0
- package/src/law_graph_usda.ts +1799 -0
- package/src/law_identity.ts +435 -0
- package/src/law_value_type.ts +230 -0
- package/src/lsp_server.ts +58 -29
- package/src/ops.ts +7 -0
- package/src/package_config.ts +141 -0
- package/src/package_dependencies.ts +108 -0
- package/src/package_dependency_usage.ts +150 -0
- package/src/package_source_imports.ts +71 -0
- package/src/plan_source_slice.ts +335 -0
- package/src/point_guard.ts +192 -0
- package/src/precondition.ts +44 -9
- package/src/read_projection_namespace.ts +1 -1
- package/src/runtime_zod.ts +44 -0
- package/src/secondary_ensure.ts +67 -14
- package/src/shape_compat.ts +1 -1
- package/src/stable_id_identity_bridge.ts +149 -0
- package/src/stable_id_lockfile.ts +140 -6
- package/src/stable_ids.ts +1 -1
- package/src/stable_ids_for_module.ts +348 -0
- package/src/stable_ids_types.ts +1 -1
- package/src/sum.ts +1 -1
- package/src/usd_state.ts +1 -1
- package/src/usda_relationship_integrity.ts +66 -0
- package/src/usda_text.ts +18 -0
- package/src/usdz.ts +78 -0
- package/src/value_object.ts +95 -0
- package/src/wire.ts +19 -0
- package/src/wire_encode.ts +22 -0
- package/src/workspace_sharding.ts +52 -36
- package/src/workspace_sharing.ts +28 -4
- package/src/zod_introspection.ts +1 -1
- package/dart/test/cluster_attested_test.dart +0 -201
- package/src/compile_engine.ts +0 -595
- package/src/manifest.ts +0 -1476
- package/src/read_projection_composition.ts +0 -117
- package/src/read_projection_manifest.ts +0 -115
- package/src/relation_composition.ts +0 -286
- package/src/usd.ts +0 -1967
- package/src/usd_layers.ts +0 -1776
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
// NOMOS — tenant law imports package contracts, never another package's private filesystem.
|
|
2
|
+
|
|
3
|
+
import { readFileSync } from "node:fs";
|
|
4
|
+
import ts from "typescript";
|
|
5
|
+
|
|
6
|
+
export interface PrivatePackageImportViolation {
|
|
7
|
+
readonly file: string;
|
|
8
|
+
readonly line: number;
|
|
9
|
+
readonly column: number;
|
|
10
|
+
readonly specifier: string;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function privatePackageSpecifier(specifier: string): boolean {
|
|
14
|
+
const normalized = specifier.replaceAll("\\", "/");
|
|
15
|
+
return normalized.split("/").includes("node_modules")
|
|
16
|
+
|| normalized.startsWith("@githolon/dsl/src/")
|
|
17
|
+
|| normalized.startsWith("@githolon/dsl/runtime/");
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/** Find imports which bypass a dependency's public package exports. */
|
|
21
|
+
export function privatePackageImportViolations(
|
|
22
|
+
file: string,
|
|
23
|
+
source = readFileSync(file, "utf8"),
|
|
24
|
+
): readonly PrivatePackageImportViolation[] {
|
|
25
|
+
const sourceFile = ts.createSourceFile(file, source, ts.ScriptTarget.Latest, true);
|
|
26
|
+
const violations: PrivatePackageImportViolation[] = [];
|
|
27
|
+
const record = (literal: ts.StringLiteralLike): void => {
|
|
28
|
+
if (!privatePackageSpecifier(literal.text)) return;
|
|
29
|
+
const position = sourceFile.getLineAndCharacterOfPosition(literal.getStart(sourceFile));
|
|
30
|
+
violations.push({
|
|
31
|
+
file,
|
|
32
|
+
line: position.line + 1,
|
|
33
|
+
column: position.character + 1,
|
|
34
|
+
specifier: literal.text,
|
|
35
|
+
});
|
|
36
|
+
};
|
|
37
|
+
const visit = (node: ts.Node): void => {
|
|
38
|
+
if (ts.isImportDeclaration(node) && ts.isStringLiteralLike(node.moduleSpecifier)) {
|
|
39
|
+
record(node.moduleSpecifier);
|
|
40
|
+
} else if (
|
|
41
|
+
ts.isExportDeclaration(node)
|
|
42
|
+
&& node.moduleSpecifier !== undefined
|
|
43
|
+
&& ts.isStringLiteralLike(node.moduleSpecifier)
|
|
44
|
+
) {
|
|
45
|
+
record(node.moduleSpecifier);
|
|
46
|
+
} else if (
|
|
47
|
+
ts.isCallExpression(node)
|
|
48
|
+
&& node.arguments.length === 1
|
|
49
|
+
&& ts.isStringLiteralLike(node.arguments[0]!)
|
|
50
|
+
&& (
|
|
51
|
+
node.expression.kind === ts.SyntaxKind.ImportKeyword
|
|
52
|
+
|| (ts.isIdentifier(node.expression) && node.expression.text === "require")
|
|
53
|
+
)
|
|
54
|
+
) {
|
|
55
|
+
record(node.arguments[0]!);
|
|
56
|
+
}
|
|
57
|
+
ts.forEachChild(node, visit);
|
|
58
|
+
};
|
|
59
|
+
visit(sourceFile);
|
|
60
|
+
return violations;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
export function assertPublicPackageImports(files: readonly string[]): void {
|
|
64
|
+
const violation = files.flatMap((file) => privatePackageImportViolations(file))[0];
|
|
65
|
+
if (violation === undefined) return;
|
|
66
|
+
throw new Error(
|
|
67
|
+
`${violation.file}:${violation.line}:${violation.column}: private package import `
|
|
68
|
+
+ `'${violation.specifier}' bypasses the package contract; import the public package entrypoint `
|
|
69
|
+
+ `or an exported subpath, and declare separately installed Nomos law in dependencies`,
|
|
70
|
+
);
|
|
71
|
+
}
|
|
@@ -0,0 +1,335 @@
|
|
|
1
|
+
// Intent-addressed execution-plan slicing.
|
|
2
|
+
//
|
|
3
|
+
// TypeScript modules are the authoring surface, not the runtime unit. A large domain file may export
|
|
4
|
+
// hundreds of independent offers; requiring that whole module in QuickJS makes the first small offer
|
|
5
|
+
// initialize every unrelated schema and plan. This compiler pass follows TypeScript symbols from the
|
|
6
|
+
// selected exports and presents esbuild with only their transitive package-local declarations.
|
|
7
|
+
|
|
8
|
+
import { existsSync, readFileSync, realpathSync } from "node:fs";
|
|
9
|
+
import path from "node:path";
|
|
10
|
+
import ts from "typescript";
|
|
11
|
+
import type { Plugin } from "esbuild";
|
|
12
|
+
|
|
13
|
+
export interface PlanSourceSliceTarget {
|
|
14
|
+
readonly modulePath: string;
|
|
15
|
+
readonly exportName: string;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
const normalized = (file: string): string => {
|
|
19
|
+
const absolute = path.resolve(file);
|
|
20
|
+
return existsSync(absolute) ? realpathSync.native(absolute) : absolute;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
const bindingIdentifiers = (name: ts.BindingName): ts.Identifier[] => {
|
|
24
|
+
if (ts.isIdentifier(name)) return [name];
|
|
25
|
+
return name.elements.flatMap((element) =>
|
|
26
|
+
ts.isOmittedExpression(element) ? [] : bindingIdentifiers(element.name));
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
export class PlanSourceSliceProgram {
|
|
30
|
+
readonly #program: ts.Program;
|
|
31
|
+
readonly #checker: ts.TypeChecker;
|
|
32
|
+
readonly #sliceRoots: readonly string[];
|
|
33
|
+
|
|
34
|
+
constructor(rootNames: readonly string[], sliceRoots: string | readonly string[]) {
|
|
35
|
+
const roots = typeof sliceRoots === "string" ? [sliceRoots] : sliceRoots;
|
|
36
|
+
this.#sliceRoots = roots.map((root) => normalized(root) + path.sep);
|
|
37
|
+
const configRoot = roots[0] ?? process.cwd();
|
|
38
|
+
const configPath = ts.findConfigFile(configRoot, existsSync, "tsconfig.json");
|
|
39
|
+
let options: ts.CompilerOptions = {
|
|
40
|
+
target: ts.ScriptTarget.ES2022,
|
|
41
|
+
module: ts.ModuleKind.ESNext,
|
|
42
|
+
moduleResolution: ts.ModuleResolutionKind.Bundler,
|
|
43
|
+
allowImportingTsExtensions: true,
|
|
44
|
+
skipLibCheck: true,
|
|
45
|
+
};
|
|
46
|
+
if (configPath !== undefined) {
|
|
47
|
+
const loaded = ts.readConfigFile(configPath, (file) => readFileSync(file, "utf8"));
|
|
48
|
+
if (loaded.error === undefined) {
|
|
49
|
+
options = {
|
|
50
|
+
...ts.parseJsonConfigFileContent(loaded.config, ts.sys, path.dirname(configPath)).options,
|
|
51
|
+
noEmit: true,
|
|
52
|
+
allowImportingTsExtensions: true,
|
|
53
|
+
};
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
this.#program = ts.createProgram({ rootNames: rootNames.map(normalized), options });
|
|
57
|
+
this.#checker = this.#program.getTypeChecker();
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
exportPosition(target: PlanSourceSliceTarget): number | undefined {
|
|
61
|
+
const source = this.#program.getSourceFile(normalized(target.modulePath));
|
|
62
|
+
const module = source === undefined ? undefined : this.#checker.getSymbolAtLocation(source);
|
|
63
|
+
const symbol = module === undefined
|
|
64
|
+
? undefined
|
|
65
|
+
: this.#checker.getExportsOfModule(module).find((candidate) => candidate.name === target.exportName);
|
|
66
|
+
const declaration = symbol?.declarations?.find((candidate) => candidate.getSourceFile() === source);
|
|
67
|
+
return declaration?.getStart(source);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Named runtime imports used by the authored program. The compiler uses this to construct the
|
|
72
|
+
* authenticated `require()` namespace from the exports plans can actually address, rather than
|
|
73
|
+
* evaluating every build-time export of a package in every QuickJS sandbox.
|
|
74
|
+
*
|
|
75
|
+
* `null` means the source used a namespace/default/side-effect import and therefore requires the
|
|
76
|
+
* complete module namespace. An absent module was not imported by the program at all.
|
|
77
|
+
*/
|
|
78
|
+
externalRuntimeImports(
|
|
79
|
+
moduleIds: readonly string[],
|
|
80
|
+
): ReadonlyMap<string, readonly string[] | null> {
|
|
81
|
+
const wanted = new Set(moduleIds);
|
|
82
|
+
const imports = new Map<string, Set<string> | null>();
|
|
83
|
+
const requireWholeModule = (id: string): void => {
|
|
84
|
+
imports.set(id, null);
|
|
85
|
+
};
|
|
86
|
+
const addNamed = (id: string, name: string): void => {
|
|
87
|
+
if (imports.get(id) === null) return;
|
|
88
|
+
const names = imports.get(id) ?? new Set<string>();
|
|
89
|
+
names.add(name);
|
|
90
|
+
imports.set(id, names);
|
|
91
|
+
};
|
|
92
|
+
for (const source of this.#program.getSourceFiles()) {
|
|
93
|
+
if (source.isDeclarationFile) continue;
|
|
94
|
+
for (const statement of source.statements) {
|
|
95
|
+
if (ts.isImportDeclaration(statement) && ts.isStringLiteral(statement.moduleSpecifier)) {
|
|
96
|
+
const id = statement.moduleSpecifier.text;
|
|
97
|
+
if (!wanted.has(id)) continue;
|
|
98
|
+
const clause = statement.importClause;
|
|
99
|
+
if (clause === undefined) {
|
|
100
|
+
requireWholeModule(id);
|
|
101
|
+
continue;
|
|
102
|
+
}
|
|
103
|
+
if (clause.isTypeOnly) continue;
|
|
104
|
+
if (clause.name !== undefined) requireWholeModule(id);
|
|
105
|
+
const bindings = clause.namedBindings;
|
|
106
|
+
if (bindings === undefined) continue;
|
|
107
|
+
if (ts.isNamespaceImport(bindings)) {
|
|
108
|
+
requireWholeModule(id);
|
|
109
|
+
continue;
|
|
110
|
+
}
|
|
111
|
+
for (const element of bindings.elements) {
|
|
112
|
+
if (!element.isTypeOnly) addNamed(id, (element.propertyName ?? element.name).text);
|
|
113
|
+
}
|
|
114
|
+
continue;
|
|
115
|
+
}
|
|
116
|
+
if (
|
|
117
|
+
ts.isExportDeclaration(statement) && statement.moduleSpecifier !== undefined &&
|
|
118
|
+
ts.isStringLiteral(statement.moduleSpecifier)
|
|
119
|
+
) {
|
|
120
|
+
const id = statement.moduleSpecifier.text;
|
|
121
|
+
if (!wanted.has(id) || statement.isTypeOnly) continue;
|
|
122
|
+
if (statement.exportClause === undefined || !ts.isNamedExports(statement.exportClause)) {
|
|
123
|
+
requireWholeModule(id);
|
|
124
|
+
continue;
|
|
125
|
+
}
|
|
126
|
+
for (const element of statement.exportClause.elements) {
|
|
127
|
+
if (!element.isTypeOnly) addNamed(id, (element.propertyName ?? element.name).text);
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return new Map(
|
|
133
|
+
[...imports].map(([id, names]) => [
|
|
134
|
+
id,
|
|
135
|
+
names === null ? null : [...names].sort(),
|
|
136
|
+
]),
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
plugin(targets: readonly PlanSourceSliceTarget[]): Plugin {
|
|
141
|
+
const retainedSymbols = new Set<ts.Symbol>();
|
|
142
|
+
const retainedDeclarations = new Set<ts.Declaration>();
|
|
143
|
+
const explicitTargetFiles = new Set(targets.map((target) => normalized(target.modulePath)));
|
|
144
|
+
const explicitTargetExports = new Map<string, Set<string>>();
|
|
145
|
+
for (const target of targets) {
|
|
146
|
+
const file = normalized(target.modulePath);
|
|
147
|
+
const names = explicitTargetExports.get(file) ?? new Set<string>();
|
|
148
|
+
names.add(target.exportName);
|
|
149
|
+
explicitTargetExports.set(file, names);
|
|
150
|
+
}
|
|
151
|
+
const insideSlice = (declaration: ts.Declaration): boolean => {
|
|
152
|
+
const file = normalized(declaration.getSourceFile().fileName);
|
|
153
|
+
return (explicitTargetFiles.has(file) || this.#sliceRoots.some((root) => file.startsWith(root))) &&
|
|
154
|
+
!file.includes(`${path.sep}node_modules${path.sep}`);
|
|
155
|
+
};
|
|
156
|
+
const markSymbol = (symbol: ts.Symbol | undefined): void => {
|
|
157
|
+
if (symbol === undefined || retainedSymbols.has(symbol)) return;
|
|
158
|
+
retainedSymbols.add(symbol);
|
|
159
|
+
if ((symbol.flags & ts.SymbolFlags.Alias) !== 0) {
|
|
160
|
+
try {
|
|
161
|
+
markSymbol(this.#checker.getAliasedSymbol(symbol));
|
|
162
|
+
} catch {
|
|
163
|
+
// An unresolved external package cannot contribute package-local declarations.
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
for (const declaration of symbol.declarations ?? []) {
|
|
167
|
+
if (!insideSlice(declaration) || retainedDeclarations.has(declaration)) continue;
|
|
168
|
+
retainedDeclarations.add(declaration);
|
|
169
|
+
const visit = (node: ts.Node): void => {
|
|
170
|
+
// `export { importedName }` has three symbols: the export alias, the local
|
|
171
|
+
// import binding, and the declaration in the imported module. Following only
|
|
172
|
+
// the alias jumps straight to the remote declaration and can therefore prune
|
|
173
|
+
// the local import that the emitted re-export still needs.
|
|
174
|
+
if (ts.isExportSpecifier(node)) {
|
|
175
|
+
markSymbol(this.#checker.getExportSpecifierLocalTargetSymbol(node));
|
|
176
|
+
}
|
|
177
|
+
if (ts.isIdentifier(node)) markSymbol(this.#checker.getSymbolAtLocation(node));
|
|
178
|
+
ts.forEachChild(node, visit);
|
|
179
|
+
};
|
|
180
|
+
ts.forEachChild(declaration, visit);
|
|
181
|
+
}
|
|
182
|
+
};
|
|
183
|
+
|
|
184
|
+
for (const target of targets) {
|
|
185
|
+
const file = normalized(target.modulePath);
|
|
186
|
+
const source = this.#program.getSourceFile(file);
|
|
187
|
+
const module = source === undefined ? undefined : this.#checker.getSymbolAtLocation(source);
|
|
188
|
+
const symbol = module === undefined
|
|
189
|
+
? undefined
|
|
190
|
+
: this.#checker.getExportsOfModule(module).find((candidate) => candidate.name === target.exportName);
|
|
191
|
+
if (symbol === undefined) {
|
|
192
|
+
throw new Error(`cannot slice missing export '${target.exportName}' from ${target.modulePath}`);
|
|
193
|
+
}
|
|
194
|
+
markSymbol(symbol);
|
|
195
|
+
// TypeScript's module export symbol can point at the import alias rather than
|
|
196
|
+
// the authored `export { name }` specifier. Anchor both sides explicitly so
|
|
197
|
+
// the slicer retains the local binding as well as the public export.
|
|
198
|
+
for (const statement of source?.statements ?? []) {
|
|
199
|
+
if (!ts.isExportDeclaration(statement) ||
|
|
200
|
+
statement.exportClause === undefined ||
|
|
201
|
+
!ts.isNamedExports(statement.exportClause)) continue;
|
|
202
|
+
for (const element of statement.exportClause.elements) {
|
|
203
|
+
if (element.name.text === target.exportName) {
|
|
204
|
+
retainedDeclarations.add(element);
|
|
205
|
+
markSymbol(this.#checker.getExportSpecifierLocalTargetSymbol(element));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
const hasRetainedSymbol = (symbol: ts.Symbol | undefined): boolean =>
|
|
212
|
+
symbol !== undefined && retainedSymbols.has(symbol);
|
|
213
|
+
const declarationIsRetained = (declaration: ts.Declaration): boolean => {
|
|
214
|
+
if (retainedDeclarations.has(declaration)) return true;
|
|
215
|
+
const name = (declaration as ts.NamedDeclaration).name;
|
|
216
|
+
if (name === undefined || !ts.isIdentifier(name)) return false;
|
|
217
|
+
return hasRetainedSymbol(this.#checker.getSymbolAtLocation(name));
|
|
218
|
+
};
|
|
219
|
+
const importBindingRetained = (identifier: ts.Identifier): boolean =>
|
|
220
|
+
hasRetainedSymbol(this.#checker.getSymbolAtLocation(identifier));
|
|
221
|
+
|
|
222
|
+
const sliceSource = (file: string): string | undefined => {
|
|
223
|
+
const absolute = normalized(file);
|
|
224
|
+
if (
|
|
225
|
+
(!explicitTargetFiles.has(absolute) && !this.#sliceRoots.some((root) => absolute.startsWith(root))) ||
|
|
226
|
+
absolute.includes(`${path.sep}node_modules${path.sep}`)
|
|
227
|
+
) {
|
|
228
|
+
return undefined;
|
|
229
|
+
}
|
|
230
|
+
const source = this.#program.getSourceFile(absolute);
|
|
231
|
+
if (source === undefined || source.isDeclarationFile) return undefined;
|
|
232
|
+
const statements: ts.Statement[] = [];
|
|
233
|
+
for (const statement of source.statements) {
|
|
234
|
+
if (ts.isImportDeclaration(statement)) {
|
|
235
|
+
const clause = statement.importClause;
|
|
236
|
+
// Explicit side-effect imports retain their authored semantics.
|
|
237
|
+
if (clause === undefined) {
|
|
238
|
+
statements.push(statement);
|
|
239
|
+
continue;
|
|
240
|
+
}
|
|
241
|
+
const defaultImport = clause.name !== undefined && importBindingRetained(clause.name)
|
|
242
|
+
? clause.name
|
|
243
|
+
: undefined;
|
|
244
|
+
let bindings: ts.NamedImportBindings | undefined;
|
|
245
|
+
if (clause.namedBindings !== undefined && ts.isNamespaceImport(clause.namedBindings)) {
|
|
246
|
+
if (importBindingRetained(clause.namedBindings.name)) bindings = clause.namedBindings;
|
|
247
|
+
} else if (clause.namedBindings !== undefined) {
|
|
248
|
+
const elements = clause.namedBindings.elements.filter((element) => importBindingRetained(element.name));
|
|
249
|
+
if (elements.length > 0) bindings = ts.factory.updateNamedImports(clause.namedBindings, elements);
|
|
250
|
+
}
|
|
251
|
+
if (defaultImport !== undefined || bindings !== undefined) {
|
|
252
|
+
statements.push(ts.factory.updateImportDeclaration(
|
|
253
|
+
statement,
|
|
254
|
+
statement.modifiers,
|
|
255
|
+
ts.factory.updateImportClause(clause, clause.isTypeOnly, defaultImport, bindings),
|
|
256
|
+
statement.moduleSpecifier,
|
|
257
|
+
statement.attributes,
|
|
258
|
+
));
|
|
259
|
+
}
|
|
260
|
+
continue;
|
|
261
|
+
}
|
|
262
|
+
if (ts.isVariableStatement(statement)) {
|
|
263
|
+
const declarations = statement.declarationList.declarations.filter((declaration) =>
|
|
264
|
+
retainedDeclarations.has(declaration) ||
|
|
265
|
+
bindingIdentifiers(declaration.name).some((identifier) =>
|
|
266
|
+
hasRetainedSymbol(this.#checker.getSymbolAtLocation(identifier))));
|
|
267
|
+
if (declarations.length > 0) {
|
|
268
|
+
statements.push(ts.factory.updateVariableStatement(
|
|
269
|
+
statement,
|
|
270
|
+
statement.modifiers,
|
|
271
|
+
ts.factory.updateVariableDeclarationList(statement.declarationList, declarations),
|
|
272
|
+
));
|
|
273
|
+
}
|
|
274
|
+
continue;
|
|
275
|
+
}
|
|
276
|
+
if (
|
|
277
|
+
ts.isFunctionDeclaration(statement) || ts.isClassDeclaration(statement) ||
|
|
278
|
+
ts.isInterfaceDeclaration(statement) || ts.isTypeAliasDeclaration(statement) ||
|
|
279
|
+
ts.isEnumDeclaration(statement) || ts.isModuleDeclaration(statement)
|
|
280
|
+
) {
|
|
281
|
+
if (declarationIsRetained(statement)) statements.push(statement);
|
|
282
|
+
continue;
|
|
283
|
+
}
|
|
284
|
+
if (ts.isExportDeclaration(statement)) {
|
|
285
|
+
if (statement.exportClause === undefined) {
|
|
286
|
+
statements.push(statement);
|
|
287
|
+
} else if (ts.isNamedExports(statement.exportClause)) {
|
|
288
|
+
const elements = statement.exportClause.elements.filter((element) => {
|
|
289
|
+
const exported = this.#checker.getSymbolAtLocation(element.name);
|
|
290
|
+
const local = this.#checker.getExportSpecifierLocalTargetSymbol(element);
|
|
291
|
+
return explicitTargetExports.get(absolute)?.has(element.name.text) === true ||
|
|
292
|
+
retainedDeclarations.has(element) ||
|
|
293
|
+
hasRetainedSymbol(exported) || hasRetainedSymbol(local);
|
|
294
|
+
});
|
|
295
|
+
if (elements.length > 0) {
|
|
296
|
+
statements.push(ts.factory.updateExportDeclaration(
|
|
297
|
+
statement,
|
|
298
|
+
statement.modifiers,
|
|
299
|
+
statement.isTypeOnly,
|
|
300
|
+
ts.factory.updateNamedExports(statement.exportClause, elements),
|
|
301
|
+
statement.moduleSpecifier,
|
|
302
|
+
statement.attributes,
|
|
303
|
+
));
|
|
304
|
+
}
|
|
305
|
+
}
|
|
306
|
+
continue;
|
|
307
|
+
}
|
|
308
|
+
if (ts.isExportAssignment(statement)) {
|
|
309
|
+
const symbol = this.#checker.getSymbolAtLocation(statement.expression);
|
|
310
|
+
if (hasRetainedSymbol(symbol)) statements.push(statement);
|
|
311
|
+
continue;
|
|
312
|
+
}
|
|
313
|
+
// A domain module should be declarative, but an explicit top-level statement is authored
|
|
314
|
+
// initialization and cannot be discarded merely for performance.
|
|
315
|
+
if (ts.isExpressionStatement(statement)) statements.push(statement);
|
|
316
|
+
}
|
|
317
|
+
return ts.createPrinter().printFile(ts.factory.updateSourceFile(source, statements));
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
return {
|
|
321
|
+
name: "nomos-intent-plan-slice",
|
|
322
|
+
setup(build) {
|
|
323
|
+
build.onLoad({ filter: /\.(?:[cm]?ts|tsx)$/ }, (args) => {
|
|
324
|
+
const contents = sliceSource(args.path);
|
|
325
|
+
if (contents === undefined) return undefined;
|
|
326
|
+
return {
|
|
327
|
+
contents,
|
|
328
|
+
loader: args.path.endsWith(".tsx") ? "tsx" : "ts",
|
|
329
|
+
resolveDir: path.dirname(args.path),
|
|
330
|
+
};
|
|
331
|
+
});
|
|
332
|
+
},
|
|
333
|
+
};
|
|
334
|
+
}
|
|
335
|
+
}
|
|
@@ -0,0 +1,192 @@
|
|
|
1
|
+
// NOMOS — typed point guards are canonical law, not tenant JavaScript.
|
|
2
|
+
|
|
3
|
+
import type { AggregateHandle, FieldValue, StoredFields } from "./aggregate.js";
|
|
4
|
+
import type { Field } from "./fields.js";
|
|
5
|
+
|
|
6
|
+
export type PointGuardScalar = string | number | boolean;
|
|
7
|
+
|
|
8
|
+
export interface PointGuardPathOperand {
|
|
9
|
+
readonly source: "row" | "payload";
|
|
10
|
+
readonly path: readonly string[];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export type PointGuardPredicate =
|
|
14
|
+
| { readonly kind: "exists" }
|
|
15
|
+
| { readonly kind: "absent" }
|
|
16
|
+
| { readonly kind: "present" | "missing"; readonly operand: PointGuardPathOperand }
|
|
17
|
+
| {
|
|
18
|
+
readonly kind: "compare";
|
|
19
|
+
readonly operator: "eq" | "ne";
|
|
20
|
+
readonly left: PointGuardPathOperand;
|
|
21
|
+
readonly right: PointGuardPathOperand | { readonly source: "literal"; readonly value: PointGuardScalar };
|
|
22
|
+
}
|
|
23
|
+
| { readonly kind: "all" | "any"; readonly terms: readonly PointGuardPredicate[] }
|
|
24
|
+
| { readonly kind: "not"; readonly term: PointGuardPredicate };
|
|
25
|
+
|
|
26
|
+
/** The complete generic kernel-lowerable point guard carried by typed USDA. */
|
|
27
|
+
export interface PointGuardSpec {
|
|
28
|
+
readonly aggregateId: string;
|
|
29
|
+
readonly binding: string;
|
|
30
|
+
readonly state: "pre" | "post";
|
|
31
|
+
readonly idPayloadPath: readonly string[];
|
|
32
|
+
readonly predicate: PointGuardPredicate;
|
|
33
|
+
readonly rejectCode: string;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export interface PointPath<T> extends PointGuardPathOperand {
|
|
37
|
+
field<K extends keyof NonNullable<T> & string>(key: K): PointPath<NonNullable<T>[K]>;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
export interface PointRowPath<T> extends PointPath<T> {
|
|
41
|
+
field<K extends keyof NonNullable<T> & string>(key: K): PointRowPath<NonNullable<T>[K]>;
|
|
42
|
+
present(): PointGuardPredicate;
|
|
43
|
+
missing(): PointGuardPredicate;
|
|
44
|
+
eq(value: NonNullable<T> | PointPath<NonNullable<T>>): PointGuardPredicate;
|
|
45
|
+
ne(value: NonNullable<T> | PointPath<NonNullable<T>>): PointGuardPredicate;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface PointRow<F extends Record<string, Field>> {
|
|
49
|
+
exists(): PointGuardPredicate;
|
|
50
|
+
absent(): PointGuardPredicate;
|
|
51
|
+
field<K extends keyof StoredFields<F> & string>(key: K): PointRowPath<FieldValue<StoredFields<F>[K]>>;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
export interface PointPayload<P> {
|
|
55
|
+
field<K extends keyof P & string>(key: K): PointPath<P[K]>;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
export interface PointGuardContext<P, F extends Record<string, Field>> {
|
|
59
|
+
readonly row: PointRow<F>;
|
|
60
|
+
readonly intent: PointPayload<P>;
|
|
61
|
+
all(...terms: readonly PointGuardPredicate[]): PointGuardPredicate;
|
|
62
|
+
any(...terms: readonly PointGuardPredicate[]): PointGuardPredicate;
|
|
63
|
+
not(term: PointGuardPredicate): PointGuardPredicate;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export interface PointGuardDeclaration {
|
|
67
|
+
readonly id: PointPath<string>;
|
|
68
|
+
readonly holds: PointGuardPredicate;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
function path<T>(source: "row" | "payload", parts: readonly string[]): PointPath<T> {
|
|
72
|
+
return {
|
|
73
|
+
source,
|
|
74
|
+
path: parts,
|
|
75
|
+
field(key) {
|
|
76
|
+
return path(source, [...parts, key]);
|
|
77
|
+
},
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function rowPath<T>(parts: readonly string[]): PointRowPath<T> {
|
|
82
|
+
const base = path<T>("row", parts);
|
|
83
|
+
const compare = (
|
|
84
|
+
operator: "eq" | "ne",
|
|
85
|
+
value: NonNullable<T> | PointPath<NonNullable<T>>,
|
|
86
|
+
): PointGuardPredicate => ({
|
|
87
|
+
kind: "compare",
|
|
88
|
+
operator,
|
|
89
|
+
left: base,
|
|
90
|
+
right: isPath(value)
|
|
91
|
+
? value
|
|
92
|
+
: { source: "literal", value: scalar(value) },
|
|
93
|
+
});
|
|
94
|
+
return {
|
|
95
|
+
...base,
|
|
96
|
+
field(key) {
|
|
97
|
+
return rowPath([...parts, key]);
|
|
98
|
+
},
|
|
99
|
+
eq(value) {
|
|
100
|
+
return compare("eq", value);
|
|
101
|
+
},
|
|
102
|
+
ne(value) {
|
|
103
|
+
return compare("ne", value);
|
|
104
|
+
},
|
|
105
|
+
present() {
|
|
106
|
+
return { kind: "present", operand: base };
|
|
107
|
+
},
|
|
108
|
+
missing() {
|
|
109
|
+
return { kind: "missing", operand: base };
|
|
110
|
+
},
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function isPath(value: unknown): value is PointPath<unknown> {
|
|
115
|
+
return value !== null && typeof value === "object" &&
|
|
116
|
+
((value as { source?: unknown }).source === "row" || (value as { source?: unknown }).source === "payload") &&
|
|
117
|
+
Array.isArray((value as { path?: unknown }).path);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
function scalar(value: unknown): PointGuardScalar {
|
|
121
|
+
if (typeof value === "string" || typeof value === "boolean") return value;
|
|
122
|
+
if (typeof value === "number" && Number.isSafeInteger(value)) return value;
|
|
123
|
+
throw new Error("point precondition literals must be strings, booleans, or safe integers");
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
/** Build a typed point-guard AST by executing only this inert algebra builder. */
|
|
127
|
+
export function buildPointGuard<P, F extends Record<string, Field>>(
|
|
128
|
+
aggregate: AggregateHandle<string, F>,
|
|
129
|
+
build: (ctx: PointGuardContext<P, F>) => PointGuardDeclaration,
|
|
130
|
+
rejectCode: string,
|
|
131
|
+
): PointGuardSpec {
|
|
132
|
+
const declaration = build({
|
|
133
|
+
row: {
|
|
134
|
+
exists: () => ({ kind: "exists" }),
|
|
135
|
+
absent: () => ({ kind: "absent" }),
|
|
136
|
+
field: (key) => rowPath([key]),
|
|
137
|
+
},
|
|
138
|
+
intent: {
|
|
139
|
+
field: (key) => path("payload", [key]),
|
|
140
|
+
},
|
|
141
|
+
all: (...terms) => ({ kind: "all", terms }),
|
|
142
|
+
any: (...terms) => ({ kind: "any", terms }),
|
|
143
|
+
not: (term) => ({ kind: "not", term }),
|
|
144
|
+
});
|
|
145
|
+
if (declaration.id.source !== "payload" || declaration.id.path.length === 0) {
|
|
146
|
+
throw new Error("point precondition id must be a non-empty intent field path");
|
|
147
|
+
}
|
|
148
|
+
if (rejectCode.trim().length === 0) throw new Error("point precondition needs a non-empty reject code");
|
|
149
|
+
return {
|
|
150
|
+
aggregateId: aggregate.id,
|
|
151
|
+
binding: "target",
|
|
152
|
+
state: "pre",
|
|
153
|
+
idPayloadPath: declaration.id.path,
|
|
154
|
+
predicate: declaration.holds,
|
|
155
|
+
rejectCode,
|
|
156
|
+
};
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function valueAt(value: unknown, parts: readonly string[]): unknown {
|
|
160
|
+
let current = value;
|
|
161
|
+
for (const part of parts) {
|
|
162
|
+
if (current === null || typeof current !== "object") return undefined;
|
|
163
|
+
current = (current as Record<string, unknown>)[part];
|
|
164
|
+
}
|
|
165
|
+
return current;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** TypeScript twin used only by compile-time probes and the engine fallback. */
|
|
169
|
+
export function evaluatePointGuard(
|
|
170
|
+
predicate: PointGuardPredicate,
|
|
171
|
+
row: Record<string, unknown> | undefined,
|
|
172
|
+
payload: unknown,
|
|
173
|
+
): boolean {
|
|
174
|
+
switch (predicate.kind) {
|
|
175
|
+
case "exists": return row !== undefined && Object.keys(row).length > 0;
|
|
176
|
+
case "absent": return row === undefined || Object.keys(row).length === 0;
|
|
177
|
+
case "present": return valueAt(predicate.operand.source === "row" ? row : payload, predicate.operand.path) !== undefined;
|
|
178
|
+
case "missing": return valueAt(predicate.operand.source === "row" ? row : payload, predicate.operand.path) === undefined;
|
|
179
|
+
case "all": return predicate.terms.every((term) => evaluatePointGuard(term, row, payload));
|
|
180
|
+
case "any": return predicate.terms.some((term) => evaluatePointGuard(term, row, payload));
|
|
181
|
+
case "not": return !evaluatePointGuard(predicate.term, row, payload);
|
|
182
|
+
case "compare": {
|
|
183
|
+
const left = valueAt(row, predicate.left.path);
|
|
184
|
+
const right = predicate.right.source === "literal"
|
|
185
|
+
? predicate.right.value
|
|
186
|
+
: valueAt(predicate.right.source === "row" ? row : payload, predicate.right.path);
|
|
187
|
+
if (left === undefined || right === undefined) return predicate.operator === "ne";
|
|
188
|
+
const equal = JSON.stringify(left) === JSON.stringify(right);
|
|
189
|
+
return predicate.operator === "eq" ? equal : !equal;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
}
|
package/src/precondition.ts
CHANGED
|
@@ -32,10 +32,13 @@
|
|
|
32
32
|
*/
|
|
33
33
|
import type { Directive, PreconditionDescriptor } from "./directive.js";
|
|
34
34
|
import {
|
|
35
|
+
nativeReadBindings,
|
|
36
|
+
readSet,
|
|
35
37
|
refAs,
|
|
36
38
|
type InvariantRef,
|
|
37
39
|
type WorkspaceInvariantDecl,
|
|
38
40
|
} from "./framework/workspace_invariant.js";
|
|
41
|
+
import { evaluatePointGuard } from "./point_guard.js";
|
|
39
42
|
|
|
40
43
|
/** The stable ref binding name the synthesized invariant resolves the target under. */
|
|
41
44
|
const REF_NAME = "target";
|
|
@@ -74,17 +77,40 @@ export function synthesizePreconditionInvariants<P>(directive: Directive<P>): Wo
|
|
|
74
77
|
const ordinal = seen.get(key) ?? 0;
|
|
75
78
|
seen.set(key, ordinal + 1);
|
|
76
79
|
const id = preconditionInvariantId(directive.id, desc, ordinal);
|
|
80
|
+
const readId = desc.readId ?? (desc.idPayloadPath === undefined
|
|
81
|
+
? undefined
|
|
82
|
+
: {
|
|
83
|
+
source: "payload" as const,
|
|
84
|
+
path: desc.idPayloadPath,
|
|
85
|
+
prefix: "",
|
|
86
|
+
suffix: "",
|
|
87
|
+
optional: false,
|
|
88
|
+
});
|
|
89
|
+
const nativeReads = readId === undefined
|
|
90
|
+
? undefined
|
|
91
|
+
: readSet({
|
|
92
|
+
binding: REF_NAME,
|
|
93
|
+
aggregate: desc.aggregateId,
|
|
94
|
+
state: "pre",
|
|
95
|
+
id: readId,
|
|
96
|
+
});
|
|
77
97
|
return {
|
|
78
98
|
id,
|
|
79
99
|
on: directive.id,
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
100
|
+
// A payload-key/point precondition is a typed USDA point read and therefore never
|
|
101
|
+
// enters QuickJS merely to construct `{aggregate,id}`. Callback selectors remain
|
|
102
|
+
// executable only long enough for the compiler to name them as migration debt.
|
|
103
|
+
reads: nativeReads ?? ((ctx: { intent: Record<string, unknown> }): InvariantRef[] => [
|
|
104
|
+
// Preconditions judge the state the directive is attempting to extend.
|
|
105
|
+
// The gate's reserved `pre:` namespace resolves the committed pre-apply
|
|
106
|
+
// aggregate; an ordinary ref would resolve the directive's trial result
|
|
107
|
+
// and therefore make every successful create appear to be a duplicate.
|
|
108
|
+
refAs(REF_NAME, desc.aggregateId, `pre:${desc.idField(ctx.intent)}`),
|
|
109
|
+
]),
|
|
110
|
+
...(nativeReads === undefined
|
|
111
|
+
? {}
|
|
112
|
+
: { __nomosReadBindings: nativeReadBindings(nativeReads)! }),
|
|
113
|
+
assert: (snapshots: Record<string, Record<string, unknown>>, ctx) => {
|
|
88
114
|
// Hosts historically represented an unresolved ref either by omitting
|
|
89
115
|
// the key or by returning an empty projection. Treat both identically;
|
|
90
116
|
// an admitted aggregate has at least one folded field after its atomic
|
|
@@ -96,10 +122,19 @@ export function synthesizePreconditionInvariants<P>(directive: Directive<P>): Wo
|
|
|
96
122
|
desc.fieldNames.some((field) => Object.hasOwn(projected, field))
|
|
97
123
|
? projected
|
|
98
124
|
: undefined;
|
|
99
|
-
|
|
125
|
+
const holds = desc.pointGuard === undefined
|
|
126
|
+
? desc.predicate(row)
|
|
127
|
+
: evaluatePointGuard(desc.pointGuard.predicate, row, ctx?.intent);
|
|
128
|
+
return holds ? { accept: true } : { reject: desc.rejectCode };
|
|
100
129
|
},
|
|
101
130
|
...(desc.declassify !== undefined ? { declassify: desc.declassify } : {}),
|
|
102
131
|
__nomosGenerated: "directive-precondition" as const,
|
|
132
|
+
...(desc.pointGuard === undefined
|
|
133
|
+
? {}
|
|
134
|
+
: {
|
|
135
|
+
__nomosEvaluator: "point-guard-v1" as const,
|
|
136
|
+
__nomosPointGuard: desc.pointGuard,
|
|
137
|
+
}),
|
|
103
138
|
__isWorkspaceInvariant: true as const,
|
|
104
139
|
};
|
|
105
140
|
});
|