@githolon/dsl 0.114.0 → 0.116.2

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.
Files changed (86) hide show
  1. package/assets/operation-contract.wasm +0 -0
  2. package/dart/CHANGELOG.md +55 -0
  3. package/dart/lib/src/application_framework_calls.dart +7 -1
  4. package/dart/lib/src/console_enrolment.dart +7 -0
  5. package/dart/lib/src/console_http_io.dart +22 -15
  6. package/dart/lib/src/dev_cloud.dart +62 -0
  7. package/dart/lib/src/nms_generated_runtime.dart +1 -0
  8. package/dart/lib/src/operation_receipts.dart +74 -7
  9. package/dart/lib/src/reception/dart_reception.dart +26 -6
  10. package/dart/lib/src/result_usda.dart +64 -27
  11. package/dart/lib/src/runner/node-entry.mjs +4733 -1534
  12. package/dart/lib/src/runtime_v2_transport.dart +3 -0
  13. package/dart/lib/src/sync_status_projection.dart +11 -0
  14. package/dart/pubspec.lock +1 -1
  15. package/dart/pubspec.yaml +2 -2
  16. package/dart/test/console_enrolment_test.dart +47 -0
  17. package/dart/test/kernel_refused_follow_test.dart +36 -0
  18. package/dart/test/model_adoption_test.dart +49 -0
  19. package/dart/test/result_usda_test.dart +67 -0
  20. package/dart/test/runtime_projections_test.dart +41 -0
  21. package/dart_types/CHANGELOG.md +30 -0
  22. package/dart_types/lib/src/approved_model.dart +78 -1
  23. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pb.dart +528 -2
  24. package/dart_types/lib/src/proto/nomos/kernel/v1/kernel.pbenum.dart +24 -0
  25. package/dart_types/lib/src/proto/nomos/runtime/v2/correspondence_desk.pb.dart +1897 -112
  26. package/dart_types/lib/src/proto/nomos/runtime/v2/model_adoption_desk.pb.dart +98 -0
  27. package/dart_types/lib/src/proto/nomos/runtime/v2/observability_desk.pbenum.dart +11 -1
  28. package/dart_types/lib/src/proto/nomos/runtime/v2/records_desk.pb.dart +16 -0
  29. package/dart_types/lib/src/proto/nomos/runtime/v2/runtime.pb.dart +43 -2
  30. package/dart_types/lib/src/vo_canonical.dart +185 -0
  31. package/dart_types/pubspec.lock +1 -1
  32. package/dart_types/pubspec.yaml +3 -1
  33. package/dart_types/test/vo_canonical_test.dart +91 -0
  34. package/package.json +4 -2
  35. package/src/aggregate.ts +95 -0
  36. package/src/anonymous_value_interning.ts +205 -0
  37. package/src/baseline.ts +27 -0
  38. package/src/baseline_export.ts +432 -0
  39. package/src/baseline_import.ts +312 -0
  40. package/src/baseline_usda.ts +185 -0
  41. package/src/birth_key_rule.ts +472 -0
  42. package/src/births.ts +207 -24
  43. package/src/build_package.ts +47 -7
  44. package/src/build_provenance.ts +10 -0
  45. package/src/business_birth.ts +50 -4
  46. package/src/check_package_main.ts +50 -6
  47. package/src/codegen_call_proto.ts +5 -18
  48. package/src/codegen_call_usda.ts +6 -3
  49. package/src/codegen_dart.ts +114 -60
  50. package/src/codegen_dart_client.ts +52 -6
  51. package/src/codegen_dart_facade.ts +57 -35
  52. package/src/codegen_dart_framework_calls.ts +20 -1
  53. package/src/codegen_proof.ts +5 -2
  54. package/src/codegen_result_proto.ts +8 -6
  55. package/src/codegen_result_usda.ts +12 -4
  56. package/src/codegen_ts.ts +109 -10
  57. package/src/compatibility_graph.ts +41 -4
  58. package/src/compatibility_usda.ts +11 -1
  59. package/src/compile_package_main.ts +222 -52
  60. package/src/composed_wire_identity.ts +24 -0
  61. package/src/directive.ts +6 -0
  62. package/src/domain_requirements.ts +75 -0
  63. package/src/engine_entry.ts +12 -4
  64. package/src/engine_javascript.ts +12 -6
  65. package/src/fields.ts +109 -0
  66. package/src/identity_usda.ts +80 -103
  67. package/src/index.ts +35 -1
  68. package/src/intent.ts +33 -3
  69. package/src/law_check.ts +34 -0
  70. package/src/law_graph.ts +142 -1
  71. package/src/law_graph_usda.ts +27 -144
  72. package/src/law_layer_prims.ts +174 -0
  73. package/src/law_unit_selection.ts +45 -0
  74. package/src/law_units.ts +7 -6
  75. package/src/law_version.ts +108 -0
  76. package/src/package_config.ts +44 -0
  77. package/src/package_dependencies.ts +8 -0
  78. package/src/point_guard.ts +5 -1
  79. package/src/release_inspection.ts +6 -4
  80. package/src/release_publication.ts +6 -5
  81. package/src/usda_text.ts +78 -3
  82. package/src/value_identity.ts +355 -0
  83. package/src/value_object.ts +35 -4
  84. package/src/wire.ts +2 -0
  85. package/src/wire_paths.ts +17 -0
  86. package/src/workspace_routing.ts +1 -1
@@ -0,0 +1,472 @@
1
+ // NOMOS — the birth-key write check (CAP-804 / CO2-1687). A field-keyed birth names its workspace from the
2
+ // key at birth, forever; the framework enforces that at runtime with a write-once invariant on the key field
3
+ // (aggregate.ts, BIRTH_KEY_WRITE_ONCE_CODE). This compile-lane pass reads the package's TypeScript SOURCE
4
+ // and WARNS — never refuses — at the exact line of a directive that writes a key field outside the birth in
5
+ // a way the runtime rule will refuse or that means something other than the author thinks:
6
+ //
7
+ // - `create(Born).set(key, …)` / `ensure(Born, <dynamic id>).set(key, …)` — ANOTHER instance of the born
8
+ // aggregate carrying a key that names a workspace it is not (the todo profiler's `createList`);
9
+ // - `existing(Born, id).set(key, …)` / `set(instance(Born, id), key, …)` — an edit of the key, refused
10
+ // whenever the value differs;
11
+ // - `ensure(Born, <constant id>).set(key, <constant>)` — a singleton seed that writes a CONSTANT key, which
12
+ // can equal the born key for at most one workspace.
13
+ //
14
+ // NOT warned: the birth's own recipe (`birth(X, { initialise })` and the framework-generated seed), and a
15
+ // genesis SEED of the identity singleton — `ensure(Born, <constant id>).set(key, <payload value>)` — which
16
+ // is exactly what the write-once rule admits when it re-states the born key.
17
+ //
18
+ // Static and syntactic: no plan is executed, so nothing is skipped because a probe could not run. It runs
19
+ // only in the compile lane (it imports `typescript` and `node:fs`); the sealed engine never loads it.
20
+
21
+ import { existsSync, readFileSync } from "node:fs";
22
+ import path from "node:path";
23
+ import ts from "typescript";
24
+ import type { AggregateHandle } from "./aggregate.js";
25
+ import { BIRTH_KEY_WRITE_ONCE_CODE } from "./aggregate.js";
26
+ import { CHOOSING_A_BIRTH_KEY_DOC, type BusinessBirthDecl } from "./births.js";
27
+
28
+ type Mod = Record<string, unknown>;
29
+
30
+ /** One package source file the check reads. */
31
+ export interface BirthKeySource {
32
+ readonly fileName: string;
33
+ readonly text: string;
34
+ }
35
+
36
+ /** What kind of out-of-birth key write was found. */
37
+ export type BirthKeyWriteKind = "creates-another" | "edits-key" | "constant-seed";
38
+
39
+ /** A compile WARNING: a directive (or helper) writes a birth key field outside the birth recipe. */
40
+ export interface BirthKeyWriteWarning {
41
+ readonly rule: "birth-key-write";
42
+ readonly kind: BirthKeyWriteKind;
43
+ readonly aggregate: string;
44
+ readonly field: string;
45
+ /** The directive whose plan contains the write, or `undefined` for a shared helper function. */
46
+ readonly directive: string | undefined;
47
+ /** The enclosing helper function's name when the write is not lexically inside a directive. */
48
+ readonly enclosing: string | undefined;
49
+ readonly file: string;
50
+ /** 1-based. */
51
+ readonly line: number;
52
+ /** 1-based. */
53
+ readonly column: number;
54
+ readonly message: string;
55
+ }
56
+
57
+ function isBirthDecl(value: unknown): value is BusinessBirthDecl {
58
+ return !!value && typeof value === "object" && (value as { __isBusinessBirth?: unknown }).__isBusinessBirth === true;
59
+ }
60
+
61
+ function isAggregateHandle(value: unknown): value is AggregateHandle {
62
+ return !!value && typeof value === "object" && (value as { __isAggregateHandle?: unknown }).__isAggregateHandle === true;
63
+ }
64
+
65
+ interface KeyedBirth {
66
+ readonly aggregate: string;
67
+ readonly field: string;
68
+ readonly namespace: string | undefined;
69
+ }
70
+
71
+ /** The source-reading verbs that address an aggregate instance, and how a write through each is judged. */
72
+ const ORIGIN_VERBS = new Set(["create", "ensure", "existing", "instance"]);
73
+
74
+ interface Origin {
75
+ readonly verb: "create" | "ensure" | "existing" | "instance";
76
+ readonly aggregateName: string;
77
+ /** The instance-id argument (`ensure(X, id)`, `existing(X, id)`, `instance(X, id)`). */
78
+ readonly idArg: ts.Expression | undefined;
79
+ }
80
+
81
+ function unwrap(expr: ts.Expression): ts.Expression {
82
+ let current = expr;
83
+ while (
84
+ ts.isParenthesizedExpression(current) || ts.isAsExpression(current) || ts.isNonNullExpression(current) ||
85
+ ts.isTypeAssertionExpression(current) || ts.isSatisfiesExpression(current)
86
+ ) {
87
+ current = current.expression;
88
+ }
89
+ return current;
90
+ }
91
+
92
+ function stringLiteralText(node: ts.Node | undefined): string | undefined {
93
+ if (node === undefined) return undefined;
94
+ return ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) ? node.text : undefined;
95
+ }
96
+
97
+ function isFunctionLike(node: ts.Node): node is ts.SignatureDeclaration & { body?: ts.Node } {
98
+ return ts.isFunctionDeclaration(node) || ts.isFunctionExpression(node) || ts.isArrowFunction(node) ||
99
+ ts.isMethodDeclaration(node) || ts.isConstructorDeclaration(node) || ts.isGetAccessorDeclaration(node) ||
100
+ ts.isSetAccessorDeclaration(node);
101
+ }
102
+
103
+ function bindingNames(name: ts.BindingName): string[] {
104
+ if (ts.isIdentifier(name)) return [name.text];
105
+ return name.elements.flatMap((element) => (ts.isOmittedExpression(element) ? [] : bindingNames(element.name)));
106
+ }
107
+
108
+ type Binding =
109
+ | { readonly where: "parameter" }
110
+ | { readonly where: "local"; readonly initializer: ts.Expression | undefined; readonly isConst: boolean }
111
+ | { readonly where: "module"; readonly initializer: ts.Expression | undefined; readonly isConst: boolean }
112
+ | { readonly where: "import" };
113
+
114
+ function variableIn(statements: readonly ts.Statement[], name: string):
115
+ { initializer: ts.Expression | undefined; isConst: boolean } | undefined {
116
+ for (const statement of statements) {
117
+ if (!ts.isVariableStatement(statement)) continue;
118
+ const isConst = (statement.declarationList.flags & ts.NodeFlags.Const) !== 0;
119
+ for (const declaration of statement.declarationList.declarations) {
120
+ if (ts.isIdentifier(declaration.name) && declaration.name.text === name) {
121
+ return { initializer: declaration.initializer, isConst };
122
+ }
123
+ if (!ts.isIdentifier(declaration.name) && bindingNames(declaration.name).includes(name)) {
124
+ return { initializer: undefined, isConst };
125
+ }
126
+ }
127
+ }
128
+ return undefined;
129
+ }
130
+
131
+ /** Resolve an identifier to its lexical binding by walking the enclosing blocks and functions. */
132
+ function resolveBinding(identifier: ts.Identifier): Binding | undefined {
133
+ const name = identifier.text;
134
+ let node: ts.Node | undefined = identifier.parent;
135
+ while (node !== undefined) {
136
+ if (isFunctionLike(node)) {
137
+ for (const parameter of node.parameters) {
138
+ if (bindingNames(parameter.name).includes(name)) return { where: "parameter" };
139
+ }
140
+ }
141
+ if (ts.isBlock(node) || ts.isModuleBlock(node) || ts.isCaseClause(node) || ts.isDefaultClause(node)) {
142
+ const found = variableIn(node.statements, name);
143
+ if (found !== undefined) return { where: "local", ...found };
144
+ }
145
+ if (ts.isForOfStatement(node) || ts.isForInStatement(node) || ts.isForStatement(node)) {
146
+ const init = node.initializer;
147
+ if (init !== undefined && ts.isVariableDeclarationList(init) &&
148
+ init.declarations.some((declaration) => bindingNames(declaration.name).includes(name))) {
149
+ return { where: "local", initializer: undefined, isConst: false };
150
+ }
151
+ }
152
+ if (ts.isCatchClause(node) && node.variableDeclaration !== undefined &&
153
+ bindingNames(node.variableDeclaration.name).includes(name)) {
154
+ return { where: "local", initializer: undefined, isConst: false };
155
+ }
156
+ if (ts.isSourceFile(node)) {
157
+ const found = variableIn(node.statements, name);
158
+ if (found !== undefined) return { where: "module", ...found };
159
+ for (const statement of node.statements) {
160
+ if (!ts.isImportDeclaration(statement)) continue;
161
+ const clause = statement.importClause;
162
+ if (clause === undefined) continue;
163
+ if (clause.name?.text === name) return { where: "import" };
164
+ const bindings = clause.namedBindings;
165
+ if (bindings === undefined) continue;
166
+ if (ts.isNamespaceImport(bindings) && bindings.name.text === name) return { where: "import" };
167
+ if (ts.isNamedImports(bindings) && bindings.elements.some((element) => element.name.text === name)) {
168
+ return { where: "import" };
169
+ }
170
+ }
171
+ return undefined;
172
+ }
173
+ node = node.parent;
174
+ }
175
+ return undefined;
176
+ }
177
+
178
+ /** A value fixed when the law module loads — a literal, an imported binding or a module-level `const`
179
+ * (or a property of one) — never anything drawn from a plan's payload, reads or context. */
180
+ function isConstantExpression(expr: ts.Expression, depth = 0): boolean {
181
+ if (depth > 8) return false;
182
+ const node = unwrap(expr);
183
+ if (ts.isStringLiteral(node) || ts.isNoSubstitutionTemplateLiteral(node) || ts.isNumericLiteral(node)) return true;
184
+ if (ts.isTemplateExpression(node)) {
185
+ return node.templateSpans.every((span) => isConstantExpression(span.expression, depth + 1));
186
+ }
187
+ if (ts.isPropertyAccessExpression(node)) return isConstantExpression(node.expression, depth + 1);
188
+ if (ts.isIdentifier(node)) {
189
+ const binding = resolveBinding(node);
190
+ if (binding === undefined) return false;
191
+ if (binding.where === "import") return true;
192
+ if (binding.where === "module") {
193
+ return binding.isConst && (binding.initializer === undefined || isConstantExpression(binding.initializer, depth + 1));
194
+ }
195
+ if (binding.where === "local" && binding.isConst && binding.initializer !== undefined) {
196
+ return isConstantExpression(binding.initializer, depth + 1);
197
+ }
198
+ return false;
199
+ }
200
+ return false;
201
+ }
202
+
203
+ /** Follow a receiver back to the `create/ensure/existing/instance(<Aggregate>, …)` that addressed it. */
204
+ function resolveOrigin(expr: ts.Expression, depth = 0): Origin | undefined {
205
+ if (depth > 16) return undefined;
206
+ const node = unwrap(expr);
207
+ if (ts.isCallExpression(node)) {
208
+ const callee = unwrap(node.expression);
209
+ if (ts.isIdentifier(callee) && ORIGIN_VERBS.has(callee.text)) {
210
+ const target = node.arguments[0] !== undefined ? unwrap(node.arguments[0]) : undefined;
211
+ if (target !== undefined && ts.isIdentifier(target)) {
212
+ return {
213
+ verb: callee.text as Origin["verb"],
214
+ aggregateName: target.text,
215
+ idArg: node.arguments[1],
216
+ };
217
+ }
218
+ return undefined;
219
+ }
220
+ // `withMarker(set(instance(X, id), …), "ensures")` and the fluent chain `.set(…).set(…)`.
221
+ if (ts.isIdentifier(callee) && callee.text === "withMarker" && node.arguments[0] !== undefined) {
222
+ const inner = resolveOrigin(node.arguments[0], depth + 1);
223
+ const marker = stringLiteralText(node.arguments[1]);
224
+ return inner !== undefined && marker === "ensures" ? { ...inner, verb: "ensure" } : inner;
225
+ }
226
+ if (ts.isPropertyAccessExpression(callee)) return resolveOrigin(callee.expression, depth + 1);
227
+ return undefined;
228
+ }
229
+ if (ts.isIdentifier(node)) {
230
+ const binding = resolveBinding(node);
231
+ if ((binding?.where === "local" || binding?.where === "module") && binding.initializer !== undefined) {
232
+ return resolveOrigin(binding.initializer, depth + 1);
233
+ }
234
+ }
235
+ return undefined;
236
+ }
237
+
238
+ /** Is this node inside the arguments of a `birth(...)` declaration (its recipe / `initialise`)? */
239
+ function insideBirthDeclaration(node: ts.Node): boolean {
240
+ let current: ts.Node | undefined = node.parent;
241
+ while (current !== undefined) {
242
+ if (ts.isCallExpression(current)) {
243
+ const callee = unwrap(current.expression);
244
+ if (ts.isIdentifier(callee) && callee.text === "birth") return true;
245
+ }
246
+ current = current.parent;
247
+ }
248
+ return false;
249
+ }
250
+
251
+ /** The `directive("<id>")` / `intent("<id>")` whose builder chain lexically encloses `node`, if any. */
252
+ function enclosingDirective(node: ts.Node): { id?: string; enclosing?: string } {
253
+ let current: ts.Node | undefined = node.parent;
254
+ let helper: string | undefined;
255
+ while (current !== undefined) {
256
+ if (ts.isCallExpression(current) && ts.isPropertyAccessExpression(current.expression)) {
257
+ // Descend the builder chain to its root call.
258
+ let root: ts.Expression = current.expression.expression;
259
+ for (let hops = 0; hops < 64; hops++) {
260
+ const unwrapped = unwrap(root);
261
+ if (ts.isCallExpression(unwrapped)) {
262
+ const callee = unwrap(unwrapped.expression);
263
+ if (ts.isIdentifier(callee) && (callee.text === "directive" || callee.text === "intent")) {
264
+ const id = stringLiteralText(unwrapped.arguments[0]);
265
+ if (id !== undefined) return { id };
266
+ break;
267
+ }
268
+ if (ts.isPropertyAccessExpression(callee)) {
269
+ root = callee.expression;
270
+ continue;
271
+ }
272
+ }
273
+ break;
274
+ }
275
+ }
276
+ if (helper === undefined) {
277
+ if (ts.isFunctionDeclaration(current) && current.name !== undefined) helper = current.name.text;
278
+ else if ((ts.isArrowFunction(current) || ts.isFunctionExpression(current)) &&
279
+ ts.isVariableDeclaration(current.parent) && ts.isIdentifier(current.parent.name)) {
280
+ helper = current.parent.name.text;
281
+ } else if (ts.isMethodDeclaration(current) && ts.isIdentifier(current.name)) helper = current.name.text;
282
+ }
283
+ current = current.parent;
284
+ }
285
+ return helper !== undefined ? { enclosing: helper } : {};
286
+ }
287
+
288
+ function warningMessage(
289
+ kind: BirthKeyWriteKind,
290
+ birth: KeyedBirth,
291
+ where: string,
292
+ ): string {
293
+ const ref = `${birth.aggregate}.${birth.field}`;
294
+ const rule =
295
+ `'${birth.field}' is the birth key of ${birth.aggregate}${birth.namespace !== undefined ? ` (namespace '${birth.namespace}')` : ""}: ` +
296
+ `the born workspace's name was derived from it at birth and is fixed forever. Nomos enforces that at the ` +
297
+ `kernel — a write that CHANGES ${ref} is refused (\`${ref}:${BIRTH_KEY_WRITE_ONCE_CODE}\`); re-stating the ` +
298
+ `same value is admitted.`;
299
+ switch (kind) {
300
+ case "creates-another":
301
+ return `${where} writes ${ref} on ANOTHER ${birth.aggregate} instance. ${rule} That other record's ` +
302
+ `'${birth.field}' names a workspace it is not, and two records can carry one key. Give it its own id ` +
303
+ `field, or omit \`key\` on the birth so Nomos mints the workspace name. See ${CHOOSING_A_BIRTH_KEY_DOC}.`;
304
+ case "edits-key":
305
+ return `${where} writes ${ref} on an existing ${birth.aggregate}. ${rule} This write will be refused ` +
306
+ `whenever the value differs; if the business needs an editable label, keep it in a separate field. ` +
307
+ `See ${CHOOSING_A_BIRTH_KEY_DOC}.`;
308
+ case "constant-seed":
309
+ return `${where} seeds ${ref} with a CONSTANT. ${rule} A constant can equal the born key for at most ` +
310
+ `one workspace; on every workspace born with a different key this write is refused (and the record ` +
311
+ `would otherwise disagree with its own workspace name). Seed it from the birth's key value instead. ` +
312
+ `See ${CHOOSING_A_BIRTH_KEY_DOC}.`;
313
+ }
314
+ }
315
+
316
+ /** Gather the field-keyed births and, per born aggregate id, the export names its handle is bound to. */
317
+ function keyedBirthsOf(modules: readonly Mod[]): { births: KeyedBirth[]; namesByAggregate: Map<string, Set<string>> } {
318
+ const births: KeyedBirth[] = [];
319
+ const seen = new Set<BusinessBirthDecl>();
320
+ for (const mod of modules) {
321
+ for (const value of Object.values(mod)) {
322
+ if (!isBirthDecl(value) || seen.has(value)) continue;
323
+ seen.add(value);
324
+ if (value.keyMode !== "field" || value.keyField === undefined) continue;
325
+ births.push({
326
+ aggregate: value.aggregate.id,
327
+ field: value.keyField,
328
+ namespace: (value as { namespace?: string }).namespace,
329
+ });
330
+ }
331
+ }
332
+ const born = new Set(births.map((birth) => birth.aggregate));
333
+ const namesByAggregate = new Map<string, Set<string>>();
334
+ for (const mod of modules) {
335
+ for (const [name, value] of Object.entries(mod)) {
336
+ if (!isAggregateHandle(value) || !born.has(value.id)) continue;
337
+ const names = namesByAggregate.get(value.id) ?? new Set<string>();
338
+ names.add(name);
339
+ namesByAggregate.set(value.id, names);
340
+ }
341
+ }
342
+ return { births, namesByAggregate };
343
+ }
344
+
345
+ /**
346
+ * Find every write of a field-keyed birth's key field outside the birth recipe that the runtime write-once
347
+ * rule will refuse or that addresses another instance (see the module header). `sources` are the package's
348
+ * own TypeScript files; aggregate handles are matched by the export names they are bound to.
349
+ */
350
+ export function findBirthKeyWrites(
351
+ modules: readonly Mod[],
352
+ sources: readonly BirthKeySource[],
353
+ opts: { readonly root?: string } = {},
354
+ ): BirthKeyWriteWarning[] {
355
+ const { births, namesByAggregate } = keyedBirthsOf(modules);
356
+ if (births.length === 0) return [];
357
+ // aggregate export name → the births keyed on it (by field).
358
+ const byName = new Map<string, KeyedBirth[]>();
359
+ for (const birth of births) {
360
+ for (const name of namesByAggregate.get(birth.aggregate) ?? []) {
361
+ byName.set(name, [...(byName.get(name) ?? []), birth]);
362
+ }
363
+ }
364
+ const keyFields = new Set(births.map((birth) => birth.field));
365
+ const warnings: BirthKeyWriteWarning[] = [];
366
+ for (const source of sources) {
367
+ if (![...keyFields].some((field) => source.text.includes(field))) continue;
368
+ const file = ts.createSourceFile(source.fileName, source.text, ts.ScriptTarget.Latest, true, ts.ScriptKind.TS);
369
+ const report = (call: ts.CallExpression, fieldNode: ts.Expression, receiver: ts.Expression, value: ts.Expression | undefined) => {
370
+ const field = stringLiteralText(fieldNode);
371
+ if (field === undefined || !keyFields.has(field)) return;
372
+ let origin = resolveOrigin(receiver);
373
+ if (origin === undefined) return;
374
+ // `withMarker(set(instance(X, id), …), "ensures")` — the functional spelling of `ensure(X, id)`.
375
+ const wrapper = call.parent;
376
+ if (origin.verb === "instance" && wrapper !== undefined && ts.isCallExpression(wrapper) &&
377
+ wrapper.arguments[0] === call && ts.isIdentifier(unwrap(wrapper.expression)) &&
378
+ (unwrap(wrapper.expression) as ts.Identifier).text === "withMarker" &&
379
+ stringLiteralText(wrapper.arguments[1]) === "ensures") {
380
+ origin = { ...origin, verb: "ensure" };
381
+ }
382
+ const birth = byName.get(origin.aggregateName)?.find((candidate) => candidate.field === field);
383
+ if (birth === undefined) return;
384
+ if (insideBirthDeclaration(call)) return;
385
+ let kind: BirthKeyWriteKind | undefined;
386
+ if (origin.verb === "create") kind = "creates-another";
387
+ else if (origin.verb === "ensure") {
388
+ const singleton = origin.idArg !== undefined && isConstantExpression(origin.idArg);
389
+ if (!singleton) kind = "creates-another";
390
+ else if (value !== undefined && isConstantExpression(value)) kind = "constant-seed";
391
+ } else kind = "edits-key";
392
+ if (kind === undefined) return;
393
+ const { line, character } = file.getLineAndCharacterOfPosition(fieldNode.getStart(file));
394
+ const context = enclosingDirective(call);
395
+ const shownFile = opts.root !== undefined ? path.relative(opts.root, source.fileName) : source.fileName;
396
+ const where = context.id !== undefined
397
+ ? `directive '${context.id}' (${shownFile}:${line + 1})`
398
+ : `${context.enclosing !== undefined ? `function '${context.enclosing}'` : "code"} (${shownFile}:${line + 1})`;
399
+ warnings.push({
400
+ rule: "birth-key-write",
401
+ kind,
402
+ aggregate: birth.aggregate,
403
+ field,
404
+ directive: context.id,
405
+ enclosing: context.enclosing,
406
+ file: shownFile,
407
+ line: line + 1,
408
+ column: character + 1,
409
+ message: warningMessage(kind, birth, where),
410
+ });
411
+ };
412
+ const visit = (node: ts.Node): void => {
413
+ if (ts.isCallExpression(node)) {
414
+ const callee = unwrap(node.expression);
415
+ // Fluent: `<ref>.set("<key>", value)` / `.relate("<key>", target)`.
416
+ if (ts.isPropertyAccessExpression(callee) && (callee.name.text === "set" || callee.name.text === "relate") &&
417
+ node.arguments.length >= 2) {
418
+ report(node, node.arguments[0]!, callee.expression, node.arguments[1]);
419
+ }
420
+ // Functional: `set(<ref>, "<key>", value)`.
421
+ if (ts.isIdentifier(callee) && callee.text === "set" && node.arguments.length >= 3) {
422
+ report(node, node.arguments[1]!, node.arguments[0]!, node.arguments[2]);
423
+ }
424
+ }
425
+ ts.forEachChild(node, visit);
426
+ };
427
+ visit(file);
428
+ }
429
+ return warnings.sort((a, b) => a.file.localeCompare(b.file) || a.line - b.line || a.column - b.column);
430
+ }
431
+
432
+ const SOURCE_EXTENSIONS = [".ts", ".mts", ".tsx"];
433
+
434
+ function resolveLocal(fromFile: string, specifier: string): string | undefined {
435
+ if (!specifier.startsWith(".")) return undefined;
436
+ const base = path.resolve(path.dirname(fromFile), specifier);
437
+ const candidates = [
438
+ base.replace(/\.js$/, ".ts").replace(/\.mjs$/, ".mts").replace(/\.jsx$/, ".tsx"),
439
+ base,
440
+ ...SOURCE_EXTENSIONS.map((extension) => base + extension),
441
+ ...SOURCE_EXTENSIONS.map((extension) => path.join(base, `index${extension}`)),
442
+ ];
443
+ return candidates.find((candidate) =>
444
+ SOURCE_EXTENSIONS.some((extension) => candidate.endsWith(extension)) && existsSync(candidate));
445
+ }
446
+
447
+ /** The package-local TypeScript sources reachable from `entries` by relative imports, inside `root`. */
448
+ export function collectPackageSources(entries: readonly string[], root: string): BirthKeySource[] {
449
+ const inside = path.resolve(root) + path.sep;
450
+ const seen = new Set<string>();
451
+ const out: BirthKeySource[] = [];
452
+ const queue = entries.map((entry) => path.resolve(entry));
453
+ while (queue.length > 0) {
454
+ const file = queue.pop()!;
455
+ if (seen.has(file)) continue;
456
+ seen.add(file);
457
+ if (!file.startsWith(inside) || file.includes(`${path.sep}node_modules${path.sep}`) || file.endsWith(".d.ts")) continue;
458
+ if (!existsSync(file)) continue;
459
+ const text = readFileSync(file, "utf8");
460
+ out.push({ fileName: file, text });
461
+ for (const imported of ts.preProcessFile(text, true, true).importedFiles) {
462
+ const resolved = resolveLocal(file, imported.fileName);
463
+ if (resolved !== undefined && !seen.has(resolved)) queue.push(resolved);
464
+ }
465
+ }
466
+ return out.sort((a, b) => a.fileName.localeCompare(b.fileName));
467
+ }
468
+
469
+ /** Render warnings for the compiler's console. */
470
+ export function formatBirthKeyWriteWarnings(warnings: readonly BirthKeyWriteWarning[]): string[] {
471
+ return warnings.map((warning) => ` ⚠ [${warning.rule}] ${warning.file}:${warning.line}:${warning.column} ${warning.message}`);
472
+ }