@telorun/ide-support 0.8.0 → 0.10.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 (52) hide show
  1. package/README.md +3 -1
  2. package/dist/completions/import-source.js +2 -2
  3. package/dist/completions/resolve-node.d.ts +5 -0
  4. package/dist/completions/resolve-node.d.ts.map +1 -1
  5. package/dist/completions/resolve-node.js +4 -0
  6. package/dist/definition/alias-qualified-value.d.ts +23 -0
  7. package/dist/definition/alias-qualified-value.d.ts.map +1 -0
  8. package/dist/definition/alias-qualified-value.js +24 -0
  9. package/dist/definition/build-definition.d.ts +15 -8
  10. package/dist/definition/build-definition.d.ts.map +1 -1
  11. package/dist/definition/build-definition.js +35 -89
  12. package/dist/definition/manifest-navigation.d.ts +36 -0
  13. package/dist/definition/manifest-navigation.d.ts.map +1 -0
  14. package/dist/definition/manifest-navigation.js +92 -0
  15. package/dist/definition/resolve-cel-target.d.ts +17 -0
  16. package/dist/definition/resolve-cel-target.d.ts.map +1 -0
  17. package/dist/definition/resolve-cel-target.js +125 -0
  18. package/dist/definition/resolve-export-chain.d.ts +23 -0
  19. package/dist/definition/resolve-export-chain.d.ts.map +1 -0
  20. package/dist/definition/resolve-export-chain.js +89 -0
  21. package/dist/definition/resolve-kind-target.d.ts +26 -0
  22. package/dist/definition/resolve-kind-target.d.ts.map +1 -0
  23. package/dist/definition/resolve-kind-target.js +51 -0
  24. package/dist/definition/resolve-ref-target.d.ts +14 -0
  25. package/dist/definition/resolve-ref-target.d.ts.map +1 -0
  26. package/dist/definition/resolve-ref-target.js +25 -0
  27. package/dist/import-upgrades/build-import-upgrades.d.ts +46 -9
  28. package/dist/import-upgrades/build-import-upgrades.d.ts.map +1 -1
  29. package/dist/import-upgrades/build-import-upgrades.js +92 -27
  30. package/dist/import-upgrades/find-import-entries.d.ts +17 -11
  31. package/dist/import-upgrades/find-import-entries.d.ts.map +1 -1
  32. package/dist/import-upgrades/find-import-entries.js +4 -5
  33. package/dist/import-upgrades/index.d.ts +3 -2
  34. package/dist/import-upgrades/index.d.ts.map +1 -1
  35. package/dist/import-upgrades/index.js +1 -0
  36. package/dist/import-upgrades/parse-module-versions.d.ts +19 -0
  37. package/dist/import-upgrades/parse-module-versions.d.ts.map +1 -0
  38. package/dist/import-upgrades/parse-module-versions.js +29 -0
  39. package/package.json +2 -2
  40. package/src/completions/import-source.ts +2 -2
  41. package/src/completions/resolve-node.ts +9 -0
  42. package/src/definition/alias-qualified-value.ts +42 -0
  43. package/src/definition/build-definition.ts +34 -104
  44. package/src/definition/manifest-navigation.ts +124 -0
  45. package/src/definition/resolve-cel-target.ts +151 -0
  46. package/src/definition/resolve-export-chain.ts +119 -0
  47. package/src/definition/resolve-kind-target.ts +61 -0
  48. package/src/definition/resolve-ref-target.ts +34 -0
  49. package/src/import-upgrades/build-import-upgrades.ts +150 -36
  50. package/src/import-upgrades/find-import-entries.ts +27 -19
  51. package/src/import-upgrades/index.ts +8 -1
  52. package/src/import-upgrades/parse-module-versions.ts +30 -0
@@ -0,0 +1,89 @@
1
+ import { parseExportEntry, resolveExportedKinds, } from "@telorun/analyzer";
2
+ import { locateKindDefinition, locateResource, moduleDoc, moduleFiles, moduleName, } from "./manifest-navigation.js";
3
+ /** One `exports.*` list as authored. `undefined` means the block is ABSENT,
4
+ * which is not the same as an empty list — the distinction is the gate itself,
5
+ * and it is load-bearing in both halves (`AliasResolver.registerImport`,
6
+ * `ModuleContext.buildExportTable`), so it must survive here too. */
7
+ function exportList(mod, block) {
8
+ const exports = moduleDoc(mod)?.exports;
9
+ const list = exports?.[block];
10
+ if (!Array.isArray(list))
11
+ return undefined;
12
+ return list.filter((e) => typeof e === "string");
13
+ }
14
+ /** Every loaded module keyed by its `metadata.name` — what `resolveExportedKinds`
15
+ * speaks, since a canonical kind names its owning module rather than a URL. */
16
+ function modulesByName(graph) {
17
+ const byName = new Map();
18
+ for (const mod of graph.modules.values()) {
19
+ const name = moduleName(mod);
20
+ if (name && !byName.has(name))
21
+ byName.set(name, mod);
22
+ }
23
+ return byName;
24
+ }
25
+ /** Resolve the kind suffix `name` exported by the module at `targetSource` to
26
+ * the `Telo.Definition` / `Telo.Abstract` doc that owns it.
27
+ *
28
+ * The gate and the transitive re-export chain come from the analyzer's own
29
+ * `resolveExportedKinds` fixpoint rather than a local walk, so navigation
30
+ * cannot disagree with `telo check` about what an import exposes — including
31
+ * the two rules a hand-rolled walk gets wrong: `exports.kinds: []` gates
32
+ * everything while an absent block gates nothing, and a re-export FROM an
33
+ * ungated module resolves straight to it. */
34
+ export function resolveExportedKind(graph, targetSource, name) {
35
+ const target = graph.modules.get(targetSource);
36
+ if (!target)
37
+ return undefined;
38
+ // No `exports.kinds` block → every kind the module defines is importable (the
39
+ // legacy permissive default the kernel still honors for already-published
40
+ // versions). The fixpoint builds tables only from declared entries, so an
41
+ // ungated module has none to look this up in — and this is also the cheap
42
+ // path, so it settles before anything is indexed.
43
+ if (exportList(target, "kinds") === undefined) {
44
+ return locateKindDefinition(moduleFiles(target), name);
45
+ }
46
+ const targetName = moduleName(target);
47
+ if (!targetName)
48
+ return undefined;
49
+ const byName = modulesByName(graph);
50
+ const tables = resolveExportedKinds([...byName].map(([module, mod]) => ({ module, exportsKinds: exportList(mod, "kinds") })), (module, alias) => {
51
+ const owner = byName.get(module)?.owner.source;
52
+ return owner ? graph.importEdges.get(owner)?.get(alias)?.targetModuleName ?? undefined : undefined;
53
+ });
54
+ const canonical = tables.get(targetName)?.get(name);
55
+ if (!canonical)
56
+ return undefined;
57
+ // `<owningModule>.<Kind>` — a re-export resolves to its true owner, so the
58
+ // jump lands where the kind is actually declared, however many hops away.
59
+ const dot = canonical.lastIndexOf(".");
60
+ const owner = byName.get(canonical.slice(0, dot));
61
+ return owner ? locateKindDefinition(moduleFiles(owner), canonical.slice(dot + 1)) : undefined;
62
+ }
63
+ /** Resolve the instance `name` exported by the module at `targetSource`,
64
+ * following `<Alias>.<name>` re-exports transitively. `seen` bounds cyclic
65
+ * import graphs.
66
+ *
67
+ * Unlike kinds there is no permissive default: the kernel reads
68
+ * `exports.resources ?? []` and builds its export table strictly from that, so
69
+ * an absent block exports nothing and an unlisted name is an
70
+ * `UNRESOLVED_REFERENCE` — navigating to it would tell the author a wiring is
71
+ * real that the analyzer rejects. */
72
+ export function resolveExportedResource(graph, targetSource, name, seen = new Set()) {
73
+ if (seen.has(targetSource))
74
+ return undefined;
75
+ seen.add(targetSource);
76
+ const target = graph.modules.get(targetSource);
77
+ if (!target)
78
+ return undefined;
79
+ const entries = (exportList(target, "resources") ?? []).map(parseExportEntry);
80
+ const entry = entries.find((e) => e.name === name);
81
+ if (!entry)
82
+ return undefined;
83
+ // `Self.<name>` names the declaring module's own instance, exactly as a bare
84
+ // name does — `ModuleContext.buildExportTable` treats the two alike.
85
+ if (!entry.alias || entry.alias === "Self")
86
+ return locateResource(moduleFiles(target), name);
87
+ const edge = graph.importEdges.get(target.owner.source)?.get(entry.alias);
88
+ return edge ? resolveExportedResource(graph, edge.targetSource, name, seen) : undefined;
89
+ }
@@ -0,0 +1,26 @@
1
+ import type { LoadedGraph, LoadedModule } from "@telorun/analyzer";
2
+ import type { DefinitionResult } from "../types.js";
3
+ import type { ResolvedCursor } from "../completions/resolve-node.js";
4
+ import type { AliasQualifiedValue } from "./alias-qualified-value.js";
5
+ /** Whether the cursor sits in a slot whose value is an alias-qualified kind.
6
+ *
7
+ * `x-telo-ref` is an `x-telo-*` annotation, unambiguous wherever it appears.
8
+ * `extends:` is only a kind at the top level of a definition doc, so it is
9
+ * gated on both. `kind:` is positional by nature: a map carrying a
10
+ * `<Alias>.<Kind>` value under `kind` IS an inline resource declaration as far
11
+ * as the analyzer is concerned too (`resourceKindOf`), so there is no further
12
+ * structure to check — an unresolvable value simply navigates nowhere.
13
+ *
14
+ * `capability:` is deliberately absent — its values are kernel built-ins with
15
+ * no manifest to jump to (hover documents them instead). */
16
+ export declare function isKindSlot(resolved: ResolvedCursor): boolean;
17
+ /** Resolve an alias-qualified kind (`Http.Server`) to the `Telo.Definition` /
18
+ * `Telo.Abstract` doc that registers it, or — when the cursor sits on the
19
+ * alias — to the import that declares the alias.
20
+ *
21
+ * `Self.<Kind>` is the declaring module's own kind; `Telo.<Kind>` is a kernel
22
+ * built-in with no manifest, so it resolves to nothing. Across an import the
23
+ * walk honors `exports.kinds` and follows `<Inner>.<Kind>` re-exports to the
24
+ * owning module, matching what the kernel resolves the kind to at runtime. */
25
+ export declare function resolveKindTarget(graph: LoadedGraph, currentModule: LoadedModule, kind: AliasQualifiedValue): DefinitionResult | undefined;
26
+ //# sourceMappingURL=resolve-kind-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-kind-target.d.ts","sourceRoot":"","sources":["../../src/definition/resolve-kind-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,gCAAgC,CAAC;AACrE,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAMtE;;;;;;;;;;6DAU6D;AAC7D,wBAAgB,UAAU,CAAC,QAAQ,EAAE,cAAc,GAAG,OAAO,CAQ5D;AAED;;;;;;;+EAO+E;AAC/E,wBAAgB,iBAAiB,CAC/B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,YAAY,EAC3B,IAAI,EAAE,mBAAmB,GACxB,gBAAgB,GAAG,SAAS,CAkB9B"}
@@ -0,0 +1,51 @@
1
+ import { locateImport, locateKindDefinition, moduleFiles } from "./manifest-navigation.js";
2
+ import { resolveExportedKind } from "./resolve-export-chain.js";
3
+ const DEFINITION_DOC_KINDS = new Set(["Telo.Definition", "Telo.Abstract"]);
4
+ /** Whether the cursor sits in a slot whose value is an alias-qualified kind.
5
+ *
6
+ * `x-telo-ref` is an `x-telo-*` annotation, unambiguous wherever it appears.
7
+ * `extends:` is only a kind at the top level of a definition doc, so it is
8
+ * gated on both. `kind:` is positional by nature: a map carrying a
9
+ * `<Alias>.<Kind>` value under `kind` IS an inline resource declaration as far
10
+ * as the analyzer is concerned too (`resourceKindOf`), so there is no further
11
+ * structure to check — an unresolvable value simply navigates nowhere.
12
+ *
13
+ * `capability:` is deliberately absent — its values are kernel built-ins with
14
+ * no manifest to jump to (hover documents them instead). */
15
+ export function isKindSlot(resolved) {
16
+ const key = resolved.path[resolved.path.length - 1];
17
+ if (key === "x-telo-ref" || key === "kind")
18
+ return true;
19
+ return (key === "extends" &&
20
+ resolved.path.length === 1 &&
21
+ DEFINITION_DOC_KINDS.has(resolved.docKind ?? ""));
22
+ }
23
+ /** Resolve an alias-qualified kind (`Http.Server`) to the `Telo.Definition` /
24
+ * `Telo.Abstract` doc that registers it, or — when the cursor sits on the
25
+ * alias — to the import that declares the alias.
26
+ *
27
+ * `Self.<Kind>` is the declaring module's own kind; `Telo.<Kind>` is a kernel
28
+ * built-in with no manifest, so it resolves to nothing. Across an import the
29
+ * walk honors `exports.kinds` and follows `<Inner>.<Kind>` re-exports to the
30
+ * owning module, matching what the kernel resolves the kind to at runtime. */
31
+ export function resolveKindTarget(graph, currentModule, kind) {
32
+ const { alias, name, onAlias } = kind;
33
+ if (alias === undefined) {
34
+ // The legacy `<namespace>/<module>#<Kind>` identity form of `x-telo-ref`
35
+ // still resolves for already-published module versions, but it names a
36
+ // module this manifest need not import, so there is no alias to follow.
37
+ if (name.includes("#"))
38
+ return undefined;
39
+ return locateKindDefinition(moduleFiles(currentModule), name);
40
+ }
41
+ if (alias === "Self")
42
+ return locateKindDefinition(moduleFiles(currentModule), name);
43
+ if (alias === "Telo")
44
+ return undefined;
45
+ if (onAlias)
46
+ return locateImport(currentModule, alias);
47
+ const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
48
+ if (!edge)
49
+ return undefined;
50
+ return resolveExportedKind(graph, edge.targetSource, name);
51
+ }
@@ -0,0 +1,14 @@
1
+ import type { LoadedGraph, LoadedModule } from "@telorun/analyzer";
2
+ import type { DefinitionResult } from "../types.js";
3
+ import type { AliasQualifiedValue } from "./alias-qualified-value.js";
4
+ /** Resolve a `!ref` target to the resource instance it names.
5
+ *
6
+ * The grammar mirrors `resolveRefSentinels`: a bare name (or `Self.name`) is a
7
+ * local resource in the current module; `Alias.name` is an exported instance of
8
+ * the module the import `Alias` points at, followed transitively through
9
+ * re-exports and gated on each module's `exports.resources`. The alias half
10
+ * navigates to the import that declares it. Returns `undefined` when the target
11
+ * can't be found (a scope-local name, an instance the target does not export,
12
+ * or an import that failed to load). */
13
+ export declare function resolveRefTarget(graph: LoadedGraph, currentModule: LoadedModule, ref: AliasQualifiedValue): DefinitionResult | undefined;
14
+ //# sourceMappingURL=resolve-ref-target.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"resolve-ref-target.d.ts","sourceRoot":"","sources":["../../src/definition/resolve-ref-target.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,mBAAmB,CAAC;AACnE,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,aAAa,CAAC;AACpD,OAAO,KAAK,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AAItE;;;;;;;;yCAQyC;AACzC,wBAAgB,gBAAgB,CAC9B,KAAK,EAAE,WAAW,EAClB,aAAa,EAAE,YAAY,EAC3B,GAAG,EAAE,mBAAmB,GACvB,gBAAgB,GAAG,SAAS,CAc9B"}
@@ -0,0 +1,25 @@
1
+ import { locateImport, locateResource, moduleFiles } from "./manifest-navigation.js";
2
+ import { resolveExportedResource } from "./resolve-export-chain.js";
3
+ /** Resolve a `!ref` target to the resource instance it names.
4
+ *
5
+ * The grammar mirrors `resolveRefSentinels`: a bare name (or `Self.name`) is a
6
+ * local resource in the current module; `Alias.name` is an exported instance of
7
+ * the module the import `Alias` points at, followed transitively through
8
+ * re-exports and gated on each module's `exports.resources`. The alias half
9
+ * navigates to the import that declares it. Returns `undefined` when the target
10
+ * can't be found (a scope-local name, an instance the target does not export,
11
+ * or an import that failed to load). */
12
+ export function resolveRefTarget(graph, currentModule, ref) {
13
+ const { alias, name, onAlias } = ref;
14
+ if (alias === undefined || alias === "Self") {
15
+ // `Self` names the declaring module itself — there is no import to jump to,
16
+ // so the alias half resolves like the name half.
17
+ return locateResource(moduleFiles(currentModule), name);
18
+ }
19
+ if (onAlias)
20
+ return locateImport(currentModule, alias);
21
+ const edge = graph.importEdges.get(currentModule.owner.source)?.get(alias);
22
+ if (!edge)
23
+ return undefined;
24
+ return resolveExportedResource(graph, edge.targetSource, name);
25
+ }
@@ -1,13 +1,28 @@
1
1
  import { type AstDocument, type Range } from "@telorun/analyzer";
2
+ /** One version of a module, as the hub reports it.
3
+ *
4
+ * `integrity` is the import pin for exactly this version (`sha256-<base64url>`
5
+ * over whatever the owning transport verifies its own reads against). It is
6
+ * absent for a version the hub tracked before it recorded pins, and for a ref
7
+ * no transport can hash — never an error, just no pin to write. */
8
+ export interface ModuleVersion {
9
+ version: string;
10
+ integrity?: string;
11
+ }
2
12
  /** Version enumeration for one version-independent base ref, newest first.
3
13
  *
4
14
  * Narrower than the full `IdeEnvironmentAdapter` on purpose. It is the only
5
15
  * environment capability an upgrade check needs, and a host that caches or
6
16
  * throttles hub traffic wraps just this — a CodeLens re-resolves far more
7
17
  * often than a completion popup opens, so the refresh cadence is the host's
8
- * policy, not this module's. A host backed by the hub passes
9
- * `adapter.listVersionsForRef`. */
10
- export type ModuleVersionLookup = (baseRef: string) => Promise<string[]>;
18
+ * policy, not this module's.
19
+ *
20
+ * Deliberately NOT `adapter.listVersionsForRef`, which answers `string[]`:
21
+ * completion offers names, an upgrade writes a pin, so the two want different
22
+ * things from one route. A host backed by the hub fetches
23
+ * `GET /module/versions` and passes the body through
24
+ * {@link parseModuleVersions}. */
25
+ export type ModuleVersionLookup = (baseRef: string) => Promise<ModuleVersion[]>;
11
26
  /** A source edit a host applies verbatim to upgrade an import. Ranges never
12
27
  * overlap, within an upgrade or across a batch, so a host may apply the whole
13
28
  * set in one pass without ordering them. */
@@ -22,18 +37,38 @@ export interface ImportUpgrade {
22
37
  source: string;
23
38
  currentVersion: string;
24
39
  latestVersion: string;
25
- /** The source that replaces it: re-pointed at `latestVersion` with the
26
- * integrity pin dropped. */
40
+ /** What the import resolves to after the edits, in the same folded form as
41
+ * `source` — re-pointed at `latestVersion`, carrying the new pin as a
42
+ * `#sha256-…` fragment when one was available. Folded rather than literal
43
+ * because where the pin physically lands depends on the shape the author
44
+ * wrote (fragment vs `integrity:` sibling), and a host showing this as a
45
+ * preview wants the resolved import, not one of two spellings of it. */
27
46
  newSource: string;
28
- /** True when the replaced import carried a pin. A host that cannot recompute
29
- * the hash should say so — the pin covers the version being replaced, and
30
- * `telo upgrade` re-pins either shape. */
47
+ /** True when the replaced import carried a pin. */
31
48
  wasPinned: boolean;
49
+ /** True when the edits leave the import pinned to `latestVersion`. False
50
+ * means no pin was available for the target version — a host should say so
51
+ * when `wasPinned`, since the rewrite silently drops a hash the author had. */
52
+ repinned: boolean;
32
53
  /** Span of the alias key — where a per-entry affordance anchors. */
33
54
  keyRange: Range;
34
55
  /** Apply all of these to upgrade this one import. */
35
56
  edits: ImportUpgradeEdit[];
36
57
  }
58
+ /** One import that is already at the newest version but carries no integrity
59
+ * pin, and for which a pin is available. Mirrors what `telo upgrade` does with
60
+ * `ensurePinned`: a rarely-released module whose version never moves would
61
+ * otherwise stay unpinned forever, because nothing ever offers to rewrite it. */
62
+ export interface ImportPin {
63
+ alias: string;
64
+ source: string;
65
+ version: string;
66
+ /** The source that replaces it: unchanged but for the appended pin. */
67
+ newSource: string;
68
+ /** Span of the alias key — where a per-entry affordance anchors. */
69
+ keyRange: Range;
70
+ edits: ImportUpgradeEdit[];
71
+ }
37
72
  /** An import that IS behind but that this module declines to rewrite. Carries
38
73
  * the same anchor and versions an {@link ImportUpgrade} does, so a host can
39
74
  * render it in place of the upgrade affordance rather than leaving the author
@@ -51,6 +86,7 @@ export interface ImportUpgradeSet {
51
86
  /** Span of the `imports:` key — where a summary affordance anchors. */
52
87
  importsKeyRange: Range;
53
88
  upgrades: ImportUpgrade[];
89
+ pins: ImportPin[];
54
90
  skipped: ImportUpgradeSkip[];
55
91
  /** Base refs whose version lookup failed. Never thrown: one unreachable ref
56
92
  * must not blank the affordances for every other import in the file. The
@@ -63,7 +99,8 @@ export interface ImportUpgradeSet {
63
99
  /**
64
100
  * Find every `imports:` entry of a module document that names a version older
65
101
  * than the newest one `listVersions` reports, and produce the source edits that
66
- * re-point it.
102
+ * re-point it — plus every entry already at the newest version that carries no
103
+ * integrity pin, and the edits that pin it.
67
104
  *
68
105
  * Skips what carries no upgradeable version: local path imports, bare URLs,
69
106
  * untagged refs, and pins that are not SemVer (an OCI digest, a moving tag like
@@ -1 +1 @@
1
- {"version":3,"file":"build-import-upgrades.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/build-import-upgrades.ts"],"names":[],"mappings":"AAAA,OAAO,EAQL,KAAK,WAAW,EAChB,KAAK,KAAK,EACX,MAAM,mBAAmB,CAAC;AAG3B;;;;;;;oCAOoC;AACpC,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,MAAM,EAAE,CAAC,CAAC;AAEzE;;6CAE6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB;iCAC6B;IAC7B,SAAS,EAAE,MAAM,CAAC;IAClB;;+CAE2C;IAC3C,SAAS,EAAE,OAAO,CAAC;IACnB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,qDAAqD;IACrD,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED;;;yDAGyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,eAAe,EAAE,KAAK,CAAC;IACvB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B;;wDAEoD;IACpD,QAAQ,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;GAaG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,mBAAmB,EACjC,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAmDvC"}
1
+ {"version":3,"file":"build-import-upgrades.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/build-import-upgrades.ts"],"names":[],"mappings":"AAAA,OAAO,EAYL,KAAK,WAAW,EAChB,KAAK,KAAK,EACX,MAAM,mBAAmB,CAAC;AAG3B;;;;;oEAKoE;AACpE,MAAM,WAAW,aAAa;IAC5B,OAAO,EAAE,MAAM,CAAC;IAChB,SAAS,CAAC,EAAE,MAAM,CAAC;CACpB;AAED;;;;;;;;;;;;mCAYmC;AACnC,MAAM,MAAM,mBAAmB,GAAG,CAAC,OAAO,EAAE,MAAM,KAAK,OAAO,CAAC,aAAa,EAAE,CAAC,CAAC;AAEhF;;6CAE6C;AAC7C,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,KAAK,CAAC;IACb,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,qDAAqD;AACrD,MAAM,WAAW,aAAa;IAC5B,KAAK,EAAE,MAAM,CAAC;IACd,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;IACf,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB;;;;;6EAKyE;IACzE,SAAS,EAAE,MAAM,CAAC;IAClB,mDAAmD;IACnD,SAAS,EAAE,OAAO,CAAC;IACnB;;oFAEgF;IAChF,QAAQ,EAAE,OAAO,CAAC;IAClB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,qDAAqD;IACrD,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED;;;kFAGkF;AAClF,MAAM,WAAW,SAAS;IACxB,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,uEAAuE;IACvE,SAAS,EAAE,MAAM,CAAC;IAClB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,KAAK,EAAE,iBAAiB,EAAE,CAAC;CAC5B;AAED;;;yDAGyD;AACzD,MAAM,WAAW,iBAAiB;IAChC,KAAK,EAAE,MAAM,CAAC;IACd,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,EAAE,MAAM,CAAC;IACtB,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB,0EAA0E;IAC1E,MAAM,EAAE,MAAM,CAAC;CAChB;AAED,MAAM,WAAW,gBAAgB;IAC/B,uEAAuE;IACvE,eAAe,EAAE,KAAK,CAAC;IACvB,QAAQ,EAAE,aAAa,EAAE,CAAC;IAC1B,IAAI,EAAE,SAAS,EAAE,CAAC;IAClB,OAAO,EAAE,iBAAiB,EAAE,CAAC;IAC7B;;wDAEoD;IACpD,QAAQ,EAAE,KAAK,CAAC;QAAE,OAAO,EAAE,MAAM,CAAC;QAAC,OAAO,EAAE,MAAM,CAAA;KAAE,CAAC,CAAC;CACvD;AAED;;;;;;;;;;;;;;GAcG;AACH,wBAAsB,mBAAmB,CACvC,IAAI,EAAE,MAAM,EACZ,YAAY,EAAE,mBAAmB,EACjC,IAAI,CAAC,EAAE,WAAW,EAAE,GACnB,OAAO,CAAC,gBAAgB,GAAG,SAAS,CAAC,CAmEvC"}
@@ -1,9 +1,10 @@
1
- import { buildLineOffsets, isLocalPathSource, isNewerModuleVersion, newestModuleVersion, parseToAst, parseVersionedRef, withRefVersion, } from "@telorun/analyzer";
1
+ import { buildLineOffsets, foldIntegrity, isCanonicalIntegrity, isLocalPathSource, isNewerModuleVersion, isSameModuleVersion, newestModuleVersion, parseModuleVersion, parseToAst, parseVersionedRef, withRefVersion, } from "@telorun/analyzer";
2
2
  import { findImportEntries } from "./find-import-entries.js";
3
3
  /**
4
4
  * Find every `imports:` entry of a module document that names a version older
5
5
  * than the newest one `listVersions` reports, and produce the source edits that
6
- * re-point it.
6
+ * re-point it — plus every entry already at the newest version that carries no
7
+ * integrity pin, and the edits that pin it.
7
8
  *
8
9
  * Skips what carries no upgradeable version: local path imports, bare URLs,
9
10
  * untagged refs, and pins that are not SemVer (an OCI digest, a moving tag like
@@ -26,70 +27,134 @@ export async function buildImportUpgrades(text, listVersions, docs) {
26
27
  return ref ? [{ entry, ref }] : [];
27
28
  });
28
29
  const failures = [];
29
- const latest = await resolveLatest([...new Set(candidates.map((c) => c.ref.baseRef))], listVersions, failures);
30
+ const known = await resolveVersions([...new Set(candidates.map((c) => c.ref.baseRef))], listVersions, failures);
30
31
  const upgrades = [];
32
+ const pins = [];
31
33
  const skipped = [];
32
34
  for (const { entry, ref } of candidates) {
33
- const newest = latest.get(ref.baseRef);
34
- if (!newest || !isNewerModuleVersion(newest, ref.version))
35
+ const versions = known.get(ref.baseRef);
36
+ if (!versions)
35
37
  continue;
36
- if (entry.integrityInline) {
38
+ const newest = newestModuleVersion(versions.map((v) => v.version));
39
+ if (!newest)
40
+ continue;
41
+ if (!isNewerModuleVersion(newest, ref.version)) {
42
+ const pin = pinInPlace(entry, ref.version, ref.integrity, versions);
43
+ if (pin)
44
+ pins.push(pin);
45
+ continue;
46
+ }
47
+ const integrity = integrityFor(versions, newest);
48
+ // A stale pin that can be neither replaced nor removed is the one case left
49
+ // that has to be declined: re-pointing the source while leaving a hash for
50
+ // the version it replaced turns the next install into a tamper error.
51
+ if (ref.integrity && !integrity && entry.integrity && !entry.integrity.lineRange) {
37
52
  skipped.push({
38
53
  alias: entry.alias,
39
54
  currentVersion: ref.version,
40
55
  latestVersion: newest,
41
56
  keyRange: entry.keyRange,
42
57
  reason: `'${entry.alias}' carries an inline 'integrity:' that shares a line with other ` +
43
- `fields, so the stale pin cannot be removed by a line edit. Run \`telo upgrade\`.`,
58
+ `fields, and no pin is published for ${newest}, so the stale one cannot be ` +
59
+ `removed by a line edit. Run \`telo upgrade\`.`,
44
60
  });
45
61
  continue;
46
62
  }
63
+ const newSource = withRefVersion(entry.source, newest);
47
64
  upgrades.push({
48
65
  alias: entry.alias,
49
66
  source: entry.source,
50
67
  currentVersion: ref.version,
51
68
  latestVersion: newest,
52
- newSource: withRefVersion(entry.source, newest),
69
+ newSource: foldIntegrity(newSource, integrity),
53
70
  wasPinned: ref.integrity != null,
71
+ repinned: integrity != null,
54
72
  keyRange: entry.keyRange,
55
- edits: buildEdits(entry, withRefVersion(entry.source, newest)),
73
+ edits: buildEdits(entry, newSource, integrity),
56
74
  });
57
75
  }
58
- return { importsKeyRange: block.keyRange, upgrades, skipped, failures };
76
+ return { importsKeyRange: block.keyRange, upgrades, pins, skipped, failures };
59
77
  }
60
- /** Re-point the source scalar, and delete a now-stale object-form `integrity:`
61
- * line. `withRefVersion` already strips an inline `#sha256-…` fragment, so the
62
- * scalar shorthand needs no second edit. Dropping the pin is not optional: it
63
- * hashes the `telo.yaml` of the version being replaced, so carrying it onto a
64
- * different version turns the next install into a tamper error. */
65
- function buildEdits(entry, newSource) {
78
+ /** The edits that re-point an entry at `newSource` and settle its pin.
79
+ *
80
+ * Where the pin is written follows the shape the author chose: an entry with an
81
+ * `integrity:` sibling keeps it (its value is replaced in place, which works in
82
+ * block and flow style alike), and everything else carries the pin inside the
83
+ * source as a `#sha256-…` fragment — the form `telo upgrade` writes.
84
+ *
85
+ * With no pin available the sibling is deleted instead. That is not optional:
86
+ * it hashes the `telo.yaml` of the version being replaced, so carrying it onto
87
+ * a different version turns the next install into a tamper error.
88
+ * `withRefVersion` has already stripped a fragment-form pin for the same
89
+ * reason, so the scalar shorthand needs no second edit. */
90
+ function buildEdits(entry, newSource, integrity) {
91
+ if (!entry.integrity) {
92
+ return [{ range: entry.sourceRange, newText: foldIntegrity(newSource, integrity) }];
93
+ }
66
94
  const edits = [{ range: entry.sourceRange, newText: newSource }];
67
- if (entry.integrityLineRange) {
68
- edits.push({ range: entry.integrityLineRange, newText: "" });
95
+ if (integrity) {
96
+ edits.push({ range: entry.integrity.valueRange, newText: integrity });
97
+ }
98
+ else if (entry.integrity.lineRange) {
99
+ edits.push({ range: entry.integrity.lineRange, newText: "" });
69
100
  }
70
101
  return edits;
71
102
  }
72
- /** Newest version per base ref, fetched once each. A ref whose lookup rejects
73
- * is recorded in `failures` and left out of the map, so it yields no upgrade
103
+ /** Pin an entry that is already at the newest version, or `undefined` when
104
+ * there is nothing to do it is pinned already, the hub published no pin for
105
+ * the version it names, or that version is not one this module will order.
106
+ *
107
+ * The version gate matters: a moving tag (`latest`) or a digest names bytes
108
+ * that are expected to change, so pinning it would break the next release
109
+ * rather than protect it. `telo upgrade` draws the same line. */
110
+ function pinInPlace(entry, version, existingIntegrity, versions) {
111
+ if (existingIntegrity != null || entry.integrity)
112
+ return undefined;
113
+ if (parseModuleVersion(version) === null)
114
+ return undefined;
115
+ const integrity = integrityFor(versions, version);
116
+ if (!integrity)
117
+ return undefined;
118
+ const newSource = foldIntegrity(entry.source, integrity);
119
+ return {
120
+ alias: entry.alias,
121
+ source: entry.source,
122
+ version,
123
+ newSource,
124
+ keyRange: entry.keyRange,
125
+ edits: [{ range: entry.sourceRange, newText: newSource }],
126
+ };
127
+ }
128
+ /** The pin the hub reports for one version, matched by SemVer identity rather
129
+ * than string equality so a `v`-prefixed tag on either side still lines up.
130
+ *
131
+ * Re-checked here even though the host's parse already did: this value is
132
+ * spliced into the author's YAML, and a caller reaching the builder through
133
+ * its own `ModuleVersionLookup` never passed through that parse. */
134
+ function integrityFor(versions, version) {
135
+ const integrity = versions.find((v) => isSameModuleVersion(v.version, version))?.integrity;
136
+ return isCanonicalIntegrity(integrity) ? integrity : undefined;
137
+ }
138
+ /** Versions per base ref, fetched once each. A ref whose lookup rejects is
139
+ * recorded in `failures` and left out of the map, so it yields no upgrade
74
140
  * rather than a wrong one. */
75
- async function resolveLatest(baseRefs, listVersions, failures) {
141
+ async function resolveVersions(baseRefs, listVersions, failures) {
76
142
  const results = await Promise.all(baseRefs.map(async (baseRef) => {
77
143
  try {
78
- const versions = await listVersions(baseRef);
79
- return { baseRef, newest: newestModuleVersion(versions) };
144
+ return { baseRef, versions: await listVersions(baseRef) };
80
145
  }
81
146
  catch (err) {
82
147
  failures.push({
83
148
  baseRef,
84
149
  message: err instanceof Error ? err.message : String(err),
85
150
  });
86
- return { baseRef, newest: undefined };
151
+ return { baseRef, versions: undefined };
87
152
  }
88
153
  }));
89
154
  const map = new Map();
90
- for (const { baseRef, newest } of results) {
91
- if (newest)
92
- map.set(baseRef, newest);
155
+ for (const { baseRef, versions } of results) {
156
+ if (versions)
157
+ map.set(baseRef, versions);
93
158
  }
94
159
  return map;
95
160
  }
@@ -15,17 +15,23 @@ export interface ImportEntry {
15
15
  * shorthand, the `source:` value for the object form. Replacing this span
16
16
  * re-points the import. */
17
17
  sourceRange: Range;
18
- /** Whole-line span of an object-form `integrity:` entry, including its
19
- * trailing newline, so a caller can delete the line. Absent for the scalar
20
- * shorthand (where the pin rides inside `sourceRange`) and for an entry that
21
- * declares no `integrity:`. */
22
- integrityLineRange?: Range;
23
- /** Set when the entry carries an `integrity:` sibling that does NOT occupy
24
- * whole lines of its own (a flow-style `{source: …, integrity: …}` map).
25
- * Deleting it would need a structural rewrite rather than a line splice, so
26
- * a caller that cannot leave the pin behind must skip this entry rather
27
- * than re-point it and strand a hash for the version it replaced. */
28
- integrityInline?: boolean;
18
+ /** Where an object-form `integrity:` sibling sits, when the entry declares
19
+ * one. Absent for the scalar shorthand there the pin rides inside
20
+ * `sourceRange` as a `#sha256-…` fragment and for an unpinned entry. */
21
+ integrity?: ImportEntryIntegrity;
22
+ }
23
+ /** The two spans a caller needs to act on an object-form `integrity:`: one to
24
+ * re-pin it, one to remove it. They are separate because a flow-style map
25
+ * (`{source: …, integrity: …}`) supports the first and not the second. */
26
+ export interface ImportEntryIntegrity {
27
+ /** Span of the hash value itself. Replacing it re-pins the entry in the shape
28
+ * the author wrote, in any YAML style. */
29
+ valueRange: Range;
30
+ /** Whole-line span of the `integrity:` entry including its trailing newline,
31
+ * so a caller with no replacement hash can delete the line outright. Absent
32
+ * when the pair shares a line with other content, where removing it would
33
+ * need a structural rewrite rather than a line splice. */
34
+ lineRange?: Range;
29
35
  }
30
36
  /** Where the `imports:` map lives in a module document. */
31
37
  export interface ImportsBlock {
@@ -1 +1 @@
1
- {"version":3,"file":"find-import-entries.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/find-import-entries.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,WAAW,EAGhB,KAAK,KAAK,EACX,MAAM,mBAAmB,CAAC;AAE3B;;;;;iEAKiE;AACjE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB;;gCAE4B;IAC5B,WAAW,EAAE,KAAK,CAAC;IACnB;;;oCAGgC;IAChC,kBAAkB,CAAC,EAAE,KAAK,CAAC;IAC3B;;;;0EAIsE;IACtE,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,QAAQ,EAAE,KAAK,CAAC;IAChB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAED;;;;;;;qEAOqE;AACrE,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,WAAW,EAAE,EACnB,WAAW,EAAE,MAAM,EAAE,GACpB,YAAY,GAAG,SAAS,CAsB1B"}
1
+ {"version":3,"file":"find-import-entries.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/find-import-entries.ts"],"names":[],"mappings":"AAAA,OAAO,EAIL,KAAK,WAAW,EAGhB,KAAK,KAAK,EACX,MAAM,mBAAmB,CAAC;AAE3B;;;;;iEAKiE;AACjE,MAAM,WAAW,WAAW;IAC1B,KAAK,EAAE,MAAM,CAAC;IACd,wEAAwE;IACxE,MAAM,EAAE,MAAM,CAAC;IACf,oEAAoE;IACpE,QAAQ,EAAE,KAAK,CAAC;IAChB;;gCAE4B;IAC5B,WAAW,EAAE,KAAK,CAAC;IACnB;;+EAE2E;IAC3E,SAAS,CAAC,EAAE,oBAAoB,CAAC;CAClC;AAED;;2EAE2E;AAC3E,MAAM,WAAW,oBAAoB;IACnC;+CAC2C;IAC3C,UAAU,EAAE,KAAK,CAAC;IAClB;;;+DAG2D;IAC3D,SAAS,CAAC,EAAE,KAAK,CAAC;CACnB;AAED,2DAA2D;AAC3D,MAAM,WAAW,YAAY;IAC3B,uEAAuE;IACvE,QAAQ,EAAE,KAAK,CAAC;IAChB,OAAO,EAAE,WAAW,EAAE,CAAC;CACxB;AAED;;;;;;;qEAOqE;AACrE,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,WAAW,EAAE,EACnB,WAAW,EAAE,MAAM,EAAE,GACpB,YAAY,GAAG,SAAS,CAsB1B"}
@@ -56,11 +56,10 @@ function readEntry(alias, keyNode, valueNode, text, lineOffsets) {
56
56
  sourceRange: toRange(sourceNode, lineOffsets),
57
57
  };
58
58
  if (integrityPair?.value && integrity !== undefined) {
59
- const lineSpan = wholeLineSpan(integrityPair.key.range[0], integrityPair.value.range[1], text, lineOffsets);
60
- if (lineSpan)
61
- entry.integrityLineRange = lineSpan;
62
- else
63
- entry.integrityInline = true;
59
+ entry.integrity = {
60
+ valueRange: toRange(integrityPair.value, lineOffsets),
61
+ lineRange: wholeLineSpan(integrityPair.key.range[0], integrityPair.value.range[1], text, lineOffsets),
62
+ };
64
63
  }
65
64
  return entry;
66
65
  }
@@ -1,5 +1,6 @@
1
1
  export { buildImportUpgrades } from "./build-import-upgrades.js";
2
- export type { ImportUpgrade, ImportUpgradeEdit, ImportUpgradeSet, ImportUpgradeSkip, ModuleVersionLookup, } from "./build-import-upgrades.js";
2
+ export type { ImportPin, ImportUpgrade, ImportUpgradeEdit, ImportUpgradeSet, ImportUpgradeSkip, ModuleVersion, ModuleVersionLookup, } from "./build-import-upgrades.js";
3
+ export { parseModuleVersions } from "./parse-module-versions.js";
3
4
  export { findImportEntries } from "./find-import-entries.js";
4
- export type { ImportEntry, ImportsBlock } from "./find-import-entries.js";
5
+ export type { ImportEntry, ImportEntryIntegrity, ImportsBlock, } from "./find-import-entries.js";
5
6
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EACV,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EAAE,WAAW,EAAE,YAAY,EAAE,MAAM,0BAA0B,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,YAAY,EACV,SAAS,EACT,aAAa,EACb,iBAAiB,EACjB,gBAAgB,EAChB,iBAAiB,EACjB,aAAa,EACb,mBAAmB,GACpB,MAAM,4BAA4B,CAAC;AACpC,OAAO,EAAE,mBAAmB,EAAE,MAAM,4BAA4B,CAAC;AACjE,OAAO,EAAE,iBAAiB,EAAE,MAAM,0BAA0B,CAAC;AAC7D,YAAY,EACV,WAAW,EACX,oBAAoB,EACpB,YAAY,GACb,MAAM,0BAA0B,CAAC"}
@@ -1,2 +1,3 @@
1
1
  export { buildImportUpgrades } from "./build-import-upgrades.js";
2
+ export { parseModuleVersions } from "./parse-module-versions.js";
2
3
  export { findImportEntries } from "./find-import-entries.js";
@@ -0,0 +1,19 @@
1
+ import type { ModuleVersion } from "./build-import-upgrades.js";
2
+ /**
3
+ * Read the hub's `GET /module/versions` payload into {@link ModuleVersion}s,
4
+ * newest first (the route's own ordering, preserved).
5
+ *
6
+ * Pure — the host owns the `fetch`, as everything else in this package does.
7
+ * It lives here rather than in each host because the shape is this package's:
8
+ * three hosts parsing the same route by hand is how one of them was left
9
+ * filtering for strings after the route started returning objects, which
10
+ * TypeScript could not catch behind the response cast and which surfaced only
11
+ * as a silently empty version list.
12
+ *
13
+ * Tolerant by design: an entry with no usable `version` is dropped rather than
14
+ * throwing, and an `integrity` that is not a canonical `sha256-<base64url>` is
15
+ * discarded rather than carried — a pin is spliced into the author's YAML, so a
16
+ * malformed one has to become "no pin", never corrupt text.
17
+ */
18
+ export declare function parseModuleVersions(payload: unknown): ModuleVersion[];
19
+ //# sourceMappingURL=parse-module-versions.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"parse-module-versions.d.ts","sourceRoot":"","sources":["../../src/import-upgrades/parse-module-versions.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,4BAA4B,CAAC;AAEhE;;;;;;;;;;;;;;;GAeG;AACH,wBAAgB,mBAAmB,CAAC,OAAO,EAAE,OAAO,GAAG,aAAa,EAAE,CAUrE"}