@telorun/analyzer 0.68.0 → 0.70.0

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 (82) hide show
  1. package/dist/analysis-registry.d.ts +12 -0
  2. package/dist/analysis-registry.d.ts.map +1 -1
  3. package/dist/analysis-registry.js +26 -0
  4. package/dist/call-graph.d.ts +80 -1
  5. package/dist/call-graph.d.ts.map +1 -1
  6. package/dist/call-graph.js +145 -12
  7. package/dist/canonical-json.d.ts +18 -0
  8. package/dist/canonical-json.d.ts.map +1 -0
  9. package/dist/canonical-json.js +26 -0
  10. package/dist/cel-access-chains.d.ts +14 -0
  11. package/dist/cel-access-chains.d.ts.map +1 -0
  12. package/dist/cel-access-chains.js +45 -0
  13. package/dist/import-resolution-diagnostics.d.ts.map +1 -1
  14. package/dist/import-resolution-diagnostics.js +22 -8
  15. package/dist/index.d.ts +7 -5
  16. package/dist/index.d.ts.map +1 -1
  17. package/dist/index.js +5 -4
  18. package/dist/loaded-types.d.ts +4 -4
  19. package/dist/loaded-types.d.ts.map +1 -1
  20. package/dist/manifest-analysis.d.ts +19 -0
  21. package/dist/manifest-analysis.d.ts.map +1 -1
  22. package/dist/manifest-analysis.js +27 -0
  23. package/dist/manifest-diff.d.ts +111 -0
  24. package/dist/manifest-diff.d.ts.map +1 -0
  25. package/dist/manifest-diff.js +130 -0
  26. package/dist/manifest-loader.d.ts +3 -4
  27. package/dist/manifest-loader.d.ts.map +1 -1
  28. package/dist/manifest-loader.js +4 -5
  29. package/dist/manifest-schemas.d.ts +2 -0
  30. package/dist/manifest-schemas.d.ts.map +1 -1
  31. package/dist/manifest-schemas.js +4 -0
  32. package/dist/module-graph.d.ts +500 -0
  33. package/dist/module-graph.d.ts.map +1 -0
  34. package/dist/module-graph.js +1389 -0
  35. package/dist/reconcile-module-versions.d.ts.map +1 -1
  36. package/dist/reconcile-module-versions.js +10 -11
  37. package/dist/release/release-plan.d.ts +1 -1
  38. package/dist/resolve-zone-containment.d.ts +9 -1
  39. package/dist/resolve-zone-containment.d.ts.map +1 -1
  40. package/dist/resolve-zone-containment.js +34 -6
  41. package/dist/resolve-zone-requirements.d.ts.map +1 -1
  42. package/dist/resolve-zone-requirements.js +4 -2
  43. package/dist/sources/default-sources.d.ts +6 -6
  44. package/dist/sources/default-sources.d.ts.map +1 -1
  45. package/dist/sources/default-sources.js +7 -8
  46. package/dist/sources/integrity.d.ts +3 -2
  47. package/dist/sources/integrity.d.ts.map +1 -1
  48. package/dist/sources/integrity.js +26 -3
  49. package/dist/sources/versioned-ref.d.ts +17 -12
  50. package/dist/sources/versioned-ref.d.ts.map +1 -1
  51. package/dist/sources/versioned-ref.js +22 -24
  52. package/dist/telo-version.d.ts +1 -1
  53. package/dist/telo-version.js +1 -1
  54. package/package.json +2 -2
  55. package/src/analysis-registry.ts +37 -0
  56. package/src/call-graph.ts +207 -14
  57. package/src/canonical-json.ts +24 -0
  58. package/src/cel-access-chains.ts +47 -0
  59. package/src/import-resolution-diagnostics.ts +24 -7
  60. package/src/index.ts +46 -5
  61. package/src/loaded-types.ts +4 -4
  62. package/src/manifest-analysis.ts +39 -0
  63. package/src/manifest-diff.ts +219 -0
  64. package/src/manifest-loader.ts +4 -5
  65. package/src/manifest-schemas.ts +4 -0
  66. package/src/module-graph.ts +1983 -0
  67. package/src/reconcile-module-versions.ts +10 -11
  68. package/src/release/release-plan.ts +1 -1
  69. package/src/resolve-zone-containment.ts +49 -9
  70. package/src/resolve-zone-requirements.ts +7 -2
  71. package/src/sources/default-sources.ts +7 -8
  72. package/src/sources/integrity.ts +28 -3
  73. package/src/sources/versioned-ref.ts +26 -28
  74. package/src/telo-version.ts +1 -1
  75. package/dist/sources/module-ref.d.ts +0 -21
  76. package/dist/sources/module-ref.d.ts.map +0 -1
  77. package/dist/sources/module-ref.js +0 -36
  78. package/dist/sources/registry-source.d.ts +0 -14
  79. package/dist/sources/registry-source.d.ts.map +0 -1
  80. package/dist/sources/registry-source.js +0 -45
  81. package/src/sources/module-ref.ts +0 -49
  82. package/src/sources/registry-source.ts +0 -52
@@ -0,0 +1,219 @@
1
+ /**
2
+ * What changed between two loads of one manifest set.
3
+ *
4
+ * The question a reconciling host asks before it touches anything: which
5
+ * resources survive an edit untouched, which have to be rebuilt, which are new,
6
+ * and which are gone. It is here rather than in the kernel because it is pure
7
+ * data in and data out and the editor wants the same answer — to show which
8
+ * resources a save would restart, in a browser, where no kernel runs.
9
+ *
10
+ * **This is the DECLARATION half of the question and only that half.** A field
11
+ * written `!cel "variables.port"` has the same declaration whatever the
12
+ * environment says, so a host that resolves configuration outside the manifest
13
+ * has to supply what moved there (`modulesWithChangedConfig`). Keeping the two
14
+ * apart is what lets this run in a browser at all.
15
+ */
16
+ import type { ResourceManifest } from "@telorun/sdk";
17
+ import { isCompiledValue } from "@telorun/sdk";
18
+ import { nodeIdFor } from "./call-graph.js";
19
+ import { canonicalJson } from "./canonical-json.js";
20
+ import { DERIVED_METADATA_FIELDS } from "./module-metadata-scope.js";
21
+
22
+ /** How one resource differs between the two sets. `unchanged` is reported as a
23
+ * bare id rather than an entry, since there is nothing to say about it. */
24
+ export type ResourceChangeKind = "added" | "removed" | "changed";
25
+
26
+ export interface DiffEntry {
27
+ /** {@link nodeIdFor} — module-scoped, so two libraries each declaring a
28
+ * resource named `store` are two entries rather than one. */
29
+ readonly id: string;
30
+ readonly change: ResourceChangeKind;
31
+ /** The declaration as it stood. Absent for an addition. */
32
+ readonly previous?: ResourceManifest;
33
+ /** The declaration as it now stands. Absent for a removal. */
34
+ readonly next?: ResourceManifest;
35
+ }
36
+
37
+ export interface ManifestDiff {
38
+ /** Additions, removals and changes. Order follows the next set, then the
39
+ * previous set for removals, so a caller rendering this reads it in
40
+ * declaration order rather than in hash order. */
41
+ readonly entries: readonly DiffEntry[];
42
+ /** Ids present in both sets with an identical declaration. */
43
+ readonly unchanged: readonly string[];
44
+ /** Ids whose current instance is no longer valid: every removal and every
45
+ * change. What a reconciling host unwinds, once it has closed this set under
46
+ * the resources that HOLD them — which is the call graph's answer, not this
47
+ * one's. */
48
+ readonly stale: readonly string[];
49
+ /** Declarations that have to be created: every addition and every change.
50
+ * Named for the kernel's own `pendingResources`, which is where they go. */
51
+ readonly pending: readonly ResourceManifest[];
52
+ }
53
+
54
+ export interface ManifestDiffOptions {
55
+ /**
56
+ * Modules whose resolved configuration moved between the two loads, by
57
+ * `metadata.module`.
58
+ *
59
+ * Every resource of such a module is reported `changed` even where its
60
+ * declaration is identical, because a declaration is not the whole of what a
61
+ * resource is built from: `!cel "variables.port"` reads the same and means
62
+ * something else once the environment behind it moves. Only the host that
63
+ * resolved that environment can know, so it is an input rather than something
64
+ * derived here.
65
+ */
66
+ readonly modulesWithChangedConfig?: ReadonlySet<string>;
67
+
68
+ /**
69
+ * Signatures of the previous set, by node id, taken while those manifests
70
+ * were still declarations.
71
+ *
72
+ * A host that INSTALLS manifests does not keep declarations: the kernel
73
+ * registers the very objects it loaded, and resolving references writes live
74
+ * instances into them — a boot target's `!ref`, or a slot nested past the
75
+ * create-time shallow copy. Signing such an object afterwards renders those
76
+ * slots opaque and reports a change that never happened, which on a module
77
+ * document means escalating every reconciliation there is.
78
+ *
79
+ * An id absent from the map is signed from its manifest, so a caller that
80
+ * really is holding declarations passes nothing.
81
+ */
82
+ readonly previousSignatures?: ReadonlyMap<string, string>;
83
+ }
84
+
85
+ /** A value that is not part of the declaration and must not be walked into:
86
+ * a live instance a host injected over a reference slot, a function, a class
87
+ * instance of any kind. Rendering one as a constant keeps the walk finite and
88
+ * acyclic; comparing two sets across the injection boundary is not supported
89
+ * and is what {@link declarationSignature} documents against. */
90
+ const OPAQUE = '"\\u0000opaque"';
91
+
92
+ const isPlainObject = (value: object): boolean => {
93
+ const proto = Object.getPrototypeOf(value);
94
+ return proto === Object.prototype || proto === null;
95
+ };
96
+
97
+ /**
98
+ * A resource's declaration, rendered so that two equal declarations render
99
+ * identically.
100
+ *
101
+ * Three things are normalized away, and each would otherwise report a change
102
+ * that is not one:
103
+ *
104
+ * - **Loader stamps** (`DERIVED_METADATA_FIELDS`). `metadata.sourceLine` is the
105
+ * load-bearing one: inserting a line anywhere in a file shifts it for every
106
+ * resource below, so leaving it in would mark a whole file changed on any
107
+ * edit and defeat the mechanism entirely.
108
+ * - **A compiled expression** renders as its source text. What the author wrote
109
+ * is the declaration; what it evaluates to depends on configuration, which
110
+ * `modulesWithChangedConfig` carries instead.
111
+ * - **Anything that is not plain data** renders as a constant. A host that has
112
+ * injected live instances over its reference slots has manifests that are no
113
+ * longer declarations, and walking one reaches a controller's object graph,
114
+ * which is cyclic. Both sides must be declarations as loaded.
115
+ *
116
+ * The result is compared as a string rather than hashed: there is no collision
117
+ * to reason about, and a missed change here is a resource that silently keeps
118
+ * running against a declaration it no longer matches.
119
+ */
120
+ export function declarationSignature(manifest: ResourceManifest): string {
121
+ const seen = new WeakSet<object>();
122
+
123
+ const render = (value: unknown): unknown => {
124
+ if (isCompiledValue(value)) {
125
+ const source = (value as { source?: unknown }).source;
126
+ return { "\u0000cel": typeof source === "string" ? source : null };
127
+ }
128
+ if (!value || typeof value !== "object") return value;
129
+ if (seen.has(value)) return "\u0000cycle";
130
+ if (Array.isArray(value)) {
131
+ seen.add(value);
132
+ return value.map(render);
133
+ }
134
+ if (!isPlainObject(value)) return OPAQUE;
135
+ seen.add(value);
136
+ const out: Record<string, unknown> = {};
137
+ for (const [key, entry] of Object.entries(value as Record<string, unknown>)) {
138
+ out[key] = render(entry);
139
+ }
140
+ return out;
141
+ };
142
+
143
+ const authoredMetadata: Record<string, unknown> = {};
144
+ const metadata = manifest.metadata as Record<string, unknown> | undefined;
145
+ for (const [key, value] of Object.entries(metadata ?? {})) {
146
+ if (DERIVED_METADATA_FIELDS.has(key)) continue;
147
+ authoredMetadata[key] = render(value);
148
+ }
149
+
150
+ const body: Record<string, unknown> = {};
151
+ for (const [key, value] of Object.entries(manifest as Record<string, unknown>)) {
152
+ if (key === "metadata") continue;
153
+ body[key] = render(value);
154
+ }
155
+
156
+ return canonicalJson({ ...body, metadata: authoredMetadata });
157
+ }
158
+
159
+ /**
160
+ * Classify every resource of two loads against each other.
161
+ *
162
+ * **Identity is `nodeIdFor`**, which is module-scoped, so a name is compared
163
+ * only against the same name in the same module. One consequence is worth
164
+ * stating because it is visible: an INLINE resource's name is synthesized from
165
+ * its position (`api_routes_0_handler`), so inserting a route above one renames
166
+ * every inline below it and this reports those as a removal plus an addition.
167
+ * That costs a restart it did not have to cost; it never misses a change, which
168
+ * is the direction that matters.
169
+ */
170
+ export function diffManifests(
171
+ previous: readonly ResourceManifest[],
172
+ next: readonly ResourceManifest[],
173
+ options: ManifestDiffOptions = {},
174
+ ): ManifestDiff {
175
+ const before = new Map<string, ResourceManifest>();
176
+ for (const manifest of previous) before.set(nodeIdFor(manifest), manifest);
177
+
178
+ const changedConfig = options.modulesWithChangedConfig;
179
+ const configMoved = (manifest: ResourceManifest): boolean => {
180
+ if (!changedConfig || changedConfig.size === 0) return false;
181
+ const module = (manifest.metadata as { module?: string } | undefined)?.module;
182
+ return module !== undefined && changedConfig.has(module);
183
+ };
184
+
185
+ const entries: DiffEntry[] = [];
186
+ const unchanged: string[] = [];
187
+ const stale: string[] = [];
188
+ const pending: ResourceManifest[] = [];
189
+ const survived = new Set<string>();
190
+
191
+ for (const manifest of next) {
192
+ const id = nodeIdFor(manifest);
193
+ const prior = before.get(id);
194
+ if (!prior) {
195
+ entries.push({ id, change: "added", next: manifest });
196
+ pending.push(manifest);
197
+ continue;
198
+ }
199
+ survived.add(id);
200
+ const priorSignature =
201
+ options.previousSignatures?.get(id) ?? declarationSignature(prior);
202
+ const same = !configMoved(manifest) && priorSignature === declarationSignature(manifest);
203
+ if (same) {
204
+ unchanged.push(id);
205
+ continue;
206
+ }
207
+ entries.push({ id, change: "changed", previous: prior, next: manifest });
208
+ stale.push(id);
209
+ pending.push(manifest);
210
+ }
211
+
212
+ for (const [id, manifest] of before) {
213
+ if (survived.has(id)) continue;
214
+ entries.push({ id, change: "removed", previous: manifest });
215
+ stale.push(id);
216
+ }
217
+
218
+ return { entries, unchanged, stale, pending };
219
+ }
@@ -146,7 +146,7 @@ export class Loader {
146
146
  * when a URL it has already canonicalised is requested again — kernel
147
147
  * load → boot and the import-controller each ask the loader for the same
148
148
  * modules. Without this fast path every duplicate request re-runs the
149
- * source's `read()` (a `fetch` for `RegistrySource`, a disk read for
149
+ * source's `read()` (a `fetch` for `HttpSource`, a disk read for
150
150
  * `LocalFileSource`). */
151
151
  private readonly urlToSource = new Map<string, string>();
152
152
 
@@ -157,8 +157,7 @@ export class Loader {
157
157
  /** Sources are resolved in order — the first whose `supports(url)` matches
158
158
  * wins. The caller (composition root) decides which concrete sources exist
159
159
  * and supplies them; `defaultSources()` bundles the browser-safe built-ins
160
- * (HTTP + registry) for the common case. `register()` prepends a source at
161
- * runtime. */
160
+ * (HTTP) for the common case. `register()` prepends a source at runtime. */
162
161
  constructor(sources: ManifestSource[] = [], options: LoaderInitOptions = {}) {
163
162
  this.sources = [...sources];
164
163
  this.celEnv = buildCelEnvironment(options.celHandlers);
@@ -181,7 +180,7 @@ export class Loader {
181
180
  // entry are populated in `urlToSource` + `fileCache` in one read.
182
181
  // Callers (kernel.load) immediately call `loadGraph(entryUrl)`
183
182
  // afterwards — without this priming, the entry file would be read
184
- // twice (twice over the network for `RegistrySource`).
183
+ // twice (twice over the network for `HttpSource`).
185
184
  const file = await this.loadFile(url);
186
185
  return file.source;
187
186
  }
@@ -469,7 +468,7 @@ export class Loader {
469
468
 
470
469
  /** Resolve an `import` URL against the file it appears in. Relative /
471
470
  * absolute-path forms run through the owning `ManifestSource`'s
472
- * `resolveRelative`; registry refs and full URLs pass through
471
+ * `resolveRelative`; scheme-qualified refs and full URLs pass through
473
472
  * unchanged. Exposed so the import-controller (and any other
474
473
  * caller-side resolver) lands on the *exact same* canonical URL the
475
474
  * loader used when walking the entry graph — divergent resolution
@@ -241,6 +241,10 @@ export const InvokeStepSchema = {
241
241
  when: {
242
242
  title: "When",
243
243
  description: "CEL guard — the dispatch is skipped when it evaluates false.",
244
+ // The same role `if:` and `while:` carry: it is the expression that
245
+ // decides whether the statement runs, and a surface showing what a step
246
+ // is conditional on should not have to know which keyword spelled it.
247
+ "x-telo-topology-role": "predicate",
244
248
  type: "string",
245
249
  },
246
250
  retry: {