@sous-io/sous 0.2.32 → 0.2.33

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 (103) hide show
  1. package/README.md +7 -6
  2. package/docs/markdown/_sidebar.md +1 -0
  3. package/docs/markdown/commands.md +8 -4
  4. package/docs/markdown/configuration.md +28 -4
  5. package/docs/markdown/repositories-authoring.md +22 -2
  6. package/docs/markdown/repositories-consuming.md +40 -11
  7. package/docs/markdown/repositories-file-formats.md +28 -13
  8. package/docs/markdown/repositories-quickstart.md +1 -1
  9. package/docs/markdown/repositories.md +8 -4
  10. package/package.json +2 -1
  11. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +25 -17
  12. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
  13. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +21 -9
  14. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  15. package/sous.config.schema.json +27 -15
  16. package/src/commands/lock/rebuild.ts +3 -6
  17. package/src/commands/namespace/show.ts +2 -2
  18. package/src/commands/recipe/show.ts +26 -9
  19. package/src/commands/repo/contribute.ts +32 -23
  20. package/src/commands/repo/gc.ts +1 -1
  21. package/src/commands/repo/remove.ts +1 -1
  22. package/src/commands/repo/search.ts +1 -1
  23. package/src/commands/vars/ask.ts +75 -54
  24. package/src/lib/build-service.ts +85 -11
  25. package/src/lib/config-schema.ts +59 -19
  26. package/src/lib/include-resolver.ts +307 -69
  27. package/src/lib/markdown-compiler.ts +273 -81
  28. package/src/lib/project-scaffold/templates.ts +19 -11
  29. package/src/lib/repos/catalog-inputs.ts +1 -1
  30. package/src/lib/repos/catalog.ts +98 -72
  31. package/src/lib/repos/declarations.ts +22 -12
  32. package/src/lib/repos/formats/index-file.ts +1 -1
  33. package/src/lib/repos/formats/recipe-manifest.ts +3 -4
  34. package/src/lib/repos/lock-service.ts +1 -1
  35. package/src/lib/repos/locked-namespace-resolver.ts +14 -6
  36. package/src/lib/repos/locked-recipe-lookup.ts +123 -0
  37. package/src/lib/repos/locked-recipes.ts +1 -1
  38. package/src/lib/repos/memories-hash-name.ts +33 -0
  39. package/src/lib/repos/namespace-resolver.ts +198 -119
  40. package/src/lib/repos/recipe-config-layers.ts +29 -1
  41. package/src/lib/repos/recipe-key-matcher.ts +62 -0
  42. package/src/lib/repos/recipe-memories.ts +175 -0
  43. package/src/lib/repos/recipe-targets.ts +27 -28
  44. package/src/lib/repos/ref-search.ts +1 -1
  45. package/src/lib/repos/release/index-builder.ts +23 -15
  46. package/src/lib/repos/release/plan.ts +14 -7
  47. package/src/lib/repos/release/settle.ts +38 -121
  48. package/src/lib/repos/release/submit-checkout.ts +42 -23
  49. package/src/lib/repos/release/tags.ts +1 -1
  50. package/src/lib/repos/resolver.ts +90 -72
  51. package/src/lib/repos/subscription-service.ts +192 -170
  52. package/src/lib/settings.ts +37 -33
  53. package/src/lib/vars/ask.ts +3 -3
  54. package/src/lib/vars/mappings.ts +17 -9
  55. package/src/services/ref-resolver/container.ts +107 -0
  56. package/src/services/ref-resolver/dependency.ts +60 -0
  57. package/src/services/ref-resolver/format.ts +197 -0
  58. package/src/services/ref-resolver/glob.ts +115 -0
  59. package/src/services/ref-resolver/hash-names.ts +155 -0
  60. package/src/services/ref-resolver/index.ts +55 -0
  61. package/src/services/ref-resolver/injectable.ts +36 -0
  62. package/src/services/ref-resolver/location.ts +62 -0
  63. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
  64. package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
  65. package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
  66. package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
  67. package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
  68. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
  69. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
  70. package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
  71. package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
  72. package/src/services/ref-resolver/named-refs.ts +144 -0
  73. package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
  74. package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
  75. package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
  76. package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
  77. package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
  78. package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
  79. package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
  80. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
  81. package/src/services/ref-resolver/parts.ts +115 -0
  82. package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
  83. package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
  84. package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
  85. package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
  86. package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
  87. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
  88. package/src/services/ref-resolver/ref-helpers.ts +62 -0
  89. package/src/services/ref-resolver/ref-picker-service.ts +204 -0
  90. package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
  91. package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
  92. package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
  93. package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
  94. package/src/services/ref-resolver/source.ts +48 -0
  95. package/src/services/ref-resolver/tokens.ts +25 -0
  96. package/src/services/ref-resolver/types.ts +155 -0
  97. package/src/templating/init-liquid-engine.ts +25 -18
  98. package/src/lib/refs/find.ts +0 -770
  99. package/src/lib/refs/index.ts +0 -14
  100. package/src/lib/refs/parse.ts +0 -844
  101. package/src/lib/refs/pick.ts +0 -147
  102. package/src/lib/refs/scopes.ts +0 -95
  103. package/src/lib/refs/settle.ts +0 -219
@@ -3,7 +3,7 @@
3
3
  *
4
4
  * Inside a recipe repository with no argument, it is that repository, as it
5
5
  * always was. Inside a project, the argument names a repository the project
6
- * knows (resolved through `src/lib/refs/`, like every reference), and the
6
+ * knows (resolved through `src/services/ref-resolver/`, like every reference), and the
7
7
  * submission runs in that repository's checkout:
8
8
  *
9
9
  * - a linked repository is read from the checkout its link points at;
@@ -23,7 +23,16 @@ import fs from "node:fs";
23
23
  import path from "node:path";
24
24
  import { ConfigError } from "../../errors.js";
25
25
  import { nonInteractiveError } from "../../interactive.js";
26
- import { findRepository, pickReference, type ReferenceRepo } from "../../refs/index.js";
26
+ import {
27
+ CatalogLookup,
28
+ RefPickArguments,
29
+ RefResolveArguments,
30
+ locationFromUrl,
31
+ repoOf,
32
+ sharedRefPicker,
33
+ sharedRefResolver,
34
+ type CatalogRepo,
35
+ } from "../../../services/ref-resolver/index.js";
27
36
  import type { RepoEntry } from "../../settings.js";
28
37
  import { isGitCheckout, remoteUrlOf, repoSlugFromUrl, sameRemote } from "../git-clone.js";
29
38
  import { globalReposDir, projectReposDir, readEffectiveLinks } from "../links.js";
@@ -103,17 +112,28 @@ export async function findSubmitCheckout(
103
112
 
104
113
  const links = readEffectiveLinks(project.sousDir, env);
105
114
  const reference = referenceRepos(project, links);
106
- const match = await pickReference(findRepository(options.repo, { repos: reference }), {
107
- search: options.repo,
108
- interactive: options.interactive,
109
- ...(options.write === undefined ? {} : { write: options.write }),
110
- details: [
111
- reference.length === 0
112
- ? " This project uses no repositories."
113
- : ` This project uses: ${reference.map((entry) => entry.name).join(", ")}.`,
114
- ],
115
- });
116
- const name = match.repo ?? match.key;
115
+ const { refs } = await sharedRefResolver().resolve(
116
+ new RefResolveArguments({
117
+ input: options.repo,
118
+ lookup: new CatalogLookup(reference),
119
+ kinds: ["repo"],
120
+ refusedIsEmpty: true,
121
+ })
122
+ );
123
+ const match = await sharedRefPicker().pick(
124
+ refs,
125
+ new RefPickArguments({
126
+ search: options.repo,
127
+ interactive: options.interactive,
128
+ ...(options.write === undefined ? {} : { write: options.write }),
129
+ details: [
130
+ reference.length === 0
131
+ ? " This project uses no repositories."
132
+ : ` This project uses: ${reference.map((entry) => entry.name).join(", ")}.`,
133
+ ],
134
+ })
135
+ );
136
+ const name = repoOf(match)!.name!;
117
137
 
118
138
  const link = links[name];
119
139
  if (link !== undefined) {
@@ -216,17 +236,16 @@ async function chooseLinked(
216
236
  function referenceRepos(
217
237
  project: SubmitProject,
218
238
  links: Record<string, { path: string }>
219
- ): ReferenceRepo[] {
220
- const repos: ReferenceRepo[] = Object.entries(project.repos).map(([name, entry]) => ({
221
- name,
222
- url: entry.url,
223
- namespaces: [],
224
- recipes: [],
225
- }));
239
+ ): CatalogRepo[] {
240
+ const known = (name: string, url: string): CatalogRepo => {
241
+ const location = locationFromUrl(url);
242
+ return { name, ...(location === undefined ? {} : { location }), namespaces: [], recipes: [] };
243
+ };
244
+ const repos: CatalogRepo[] = Object.entries(project.repos).map(([name, entry]) =>
245
+ known(name, entry.url)
246
+ );
226
247
  for (const name of Object.keys(links).sort()) {
227
- if (project.repos[name] === undefined) {
228
- repos.push({ name, url: links[name]!.path, namespaces: [], recipes: [] });
229
- }
248
+ if (project.repos[name] === undefined) repos.push(known(name, links[name]!.path));
230
249
  }
231
250
  return repos;
232
251
  }
@@ -16,7 +16,7 @@ import os from "node:os";
16
16
  import path from "node:path";
17
17
  import { runGit, type RunOptions } from "../providers/git.js";
18
18
  import { RECIPE_KEY_PATTERN } from "../formats/patterns.js";
19
- import { splitRecipeKey } from "../../refs/parse.js";
19
+ import { splitRecipeKey } from "../../../services/ref-resolver/index.js";
20
20
 
21
21
  /** One release tag, taken apart into the recipe it publishes. */
22
22
  export type RecipeTag = {
@@ -31,17 +31,26 @@ import type { LockKind } from "./formats/lockfile.js";
31
31
  import type { RecipeManifest, VariableDefinition } from "./formats/recipe-manifest.js";
32
32
  import { indexDependencyLists, type DependencyLists } from "./declarations.js";
33
33
  import {
34
+ CatalogMatcher,
35
+ RefSource,
36
+ catalogRepoOfIndex,
34
37
  formatRef,
35
- isBrowsedReading,
36
- isNamedReading,
37
- parseRef,
38
- refKey,
38
+ isNamedRef,
39
+ locationOf,
40
+ namespaceOf,
41
+ namespaceRef,
42
+ rangeOf,
43
+ recipeRef,
44
+ repoOf,
45
+ settleDependency,
46
+ sharedRefResolver,
47
+ shortKey,
39
48
  splitRecipeKey,
40
- type ParsedRef,
41
- type RefReading,
42
- } from "../refs/parse.js";
43
- import { settleBrowsed, settleDependency, settleInIndex } from "../refs/settle.js";
44
- import { RefSource } from "../refs/scopes.js";
49
+ withRepoName,
50
+ type NamedRef,
51
+ type RepoRef,
52
+ type SousRef,
53
+ } from "../../services/ref-resolver/index.js";
45
54
  import { shortNameFromIdentity } from "./identity.js";
46
55
  import type { IndexDependency } from "./formats/index-file.js";
47
56
 
@@ -51,7 +60,12 @@ export const PROJECT_REQUESTER = "project";
51
60
  /** One thing to resolve. */
52
61
  export type RefRequest = {
53
62
  /** The ref, already parsed. */
54
- ref: ParsedRef;
63
+ ref: NamedRef;
64
+ /**
65
+ * The version range every recipe of a namespace ref must satisfy, and the
66
+ * range of a recipe ref that carries none of its own.
67
+ */
68
+ range?: string;
55
69
  /** Whether prerelease versions may take part in range matching. */
56
70
  prerelease?: boolean;
57
71
  /**
@@ -209,12 +223,12 @@ type WorkItem = RefRequest & {
209
223
  /** The dependency exactly as the manifest wrote it, for messages. */
210
224
  written: string;
211
225
  /**
212
- * A browser path the dependency named, settled through the repository's
213
- * index once the project has it. Until then `ref` holds no namespace.
226
+ * The repository ref a browser path dependency was read as (its location,
227
+ * its path and the range the manifest wrote), settled through the
228
+ * repository's index once the project has it. Until then `ref` holds no
229
+ * namespace.
214
230
  */
215
- browsed?: string;
216
- /** The range the manifest wrote, applied once a browsed path is settled. */
217
- range?: string;
231
+ browsed?: RepoRef;
218
232
  /** What the declaring version's index records, for pins once a browsed path is settled. */
219
233
  pins?: Record<string, IndexDependency>;
220
234
  };
@@ -259,26 +273,25 @@ function dependencyRequest(
259
273
  ...(parent.prerelease ? { prerelease: true } : {}),
260
274
  };
261
275
 
262
- if (isBrowsedReading(reading)) {
276
+ if (reading.kind === "repo" && reading.browsed !== undefined) {
263
277
  return {
264
- ref: { namespace: "" },
278
+ ref: namespaceRef(""),
265
279
  ...base,
266
280
  remote: {
267
281
  ...remoteOf(reading, written),
268
- browsed: reading.browsed,
269
- ...(reading.range === undefined ? {} : { range: reading.range }),
282
+ browsed: reading,
270
283
  ...(parent.dependencies === undefined ? {} : { pins: parent.dependencies }),
271
284
  },
272
285
  };
273
286
  }
274
287
  /* c8 ignore next */
275
- if (!isNamedReading(reading)) throw unsettledDependencyError(written, parent);
288
+ if (!isNamedRef(reading)) throw unsettledDependencyError(written, parent);
276
289
 
277
- const pinned = parent.dependencies?.[refKey(reading)];
290
+ const pinned = parent.dependencies?.[shortKey(reading)];
278
291
  const ref = pinnedRef(reading, pinned);
279
292
 
280
- if (reading.location === undefined) {
281
- return { ref: { ...ref, repo: parent.repo }, ...base };
293
+ if (locationOf(reading) === undefined) {
294
+ return { ref: withRepoName(ref, parent.repo), ...base };
282
295
  }
283
296
 
284
297
  const remote = remoteOf(reading, written);
@@ -296,22 +309,19 @@ function dependencyRequest(
296
309
  * @param reading - The reading.
297
310
  * @param pinned - What the declaring version's index recorded for it.
298
311
  */
299
- function pinnedRef(reading: ParsedRef, pinned: IndexDependency | undefined): ParsedRef {
300
- const ref: ParsedRef = { namespace: reading.namespace };
301
- if (reading.recipe !== undefined) {
302
- ref.recipe = reading.recipe;
303
- const range = pinned?.version ?? reading.range ?? pinned?.range;
304
- if (range !== undefined) ref.range = range;
305
- }
306
- return ref;
312
+ function pinnedRef(reading: NamedRef, pinned: IndexDependency | undefined): NamedRef {
313
+ const namespace = namespaceOf(reading)!.name;
314
+ if (reading.kind === "namespace") return namespaceRef(namespace);
315
+ const range = pinned?.version ?? reading.range ?? pinned?.range;
316
+ return recipeRef(namespace, reading.name, range === undefined ? {} : { range });
307
317
  }
308
318
 
309
319
  /** Where a located reading's repository lives, as a request carries it. */
310
320
  function remoteOf(
311
- reading: RefReading,
321
+ reading: SousRef,
312
322
  written: string
313
323
  ): NonNullable<WorkItem["remote"]> {
314
- const location = reading.location!;
324
+ const location = locationOf(reading)!;
315
325
  return {
316
326
  identity: location.identity,
317
327
  url: location.url,
@@ -327,12 +337,13 @@ function remoteOf(
327
337
  * @param reading - One reading of a dependency.
328
338
  * @param context - The added repositories and their cached indexes.
329
339
  */
330
- function knownLocally(reading: RefReading, context: ResolveContext): boolean {
331
- if (reading.location === undefined) return false;
332
- const added = repoNamedByIdentity(context.repos, reading.location.identity);
340
+ function knownLocally(reading: SousRef, context: ResolveContext): boolean {
341
+ const location = locationOf(reading);
342
+ if (location === undefined) return false;
343
+ const added = repoNamedByIdentity(context.repos, location.identity);
333
344
  const index = added === undefined ? undefined : context.indexes.get(added);
334
345
  if (index === undefined) return false;
335
- return settleInIndex(reading, { namespaces: Object.keys(index.namespaces), recipes: index.recipes }).length > 0;
346
+ return new CatalogMatcher([catalogRepoOfIndex(index, { location })]).match(reading).length > 0;
336
347
  }
337
348
 
338
349
  /**
@@ -343,11 +354,11 @@ function knownLocally(reading: RefReading, context: ResolveContext): boolean {
343
354
  * @param parent - The recipe that declared it.
344
355
  */
345
356
  function unsettledDependencyError(written: string, parent: ResolvedRecipe): ConfigError {
346
- const readings = parseRef(written, RefSource.Manifest);
357
+ const { refs } = sharedRefResolver().parse(written, RefSource.Manifest);
347
358
  return new ConfigError(
348
359
  `The dependency '${written.trim()}' of the recipe '${parent.key}' can be read ` +
349
- `${readings.length} ways, and nothing says which one it means:\n` +
350
- readings.map((reading) => ` ${formatRef(reading)}`).join("\n") +
360
+ `${refs.length} ways, and nothing says which one it means:\n` +
361
+ refs.map((reading) => ` ${formatRef(reading)}`).join("\n") +
351
362
  `\n A release records which one it settled on in the index; the index of ` +
352
363
  `'${parent.repo}' records none for this version. Release the recipe again with this ` +
353
364
  `version of sous, or write the dependency so it reads one way, as one of the forms above.`
@@ -362,15 +373,16 @@ function unsettledDependencyError(written: string, parent: ResolvedRecipe): Conf
362
373
  */
363
374
  function settleBrowsedItem(item: WorkItem, index: IndexFile | undefined): void {
364
375
  const remote = item.remote!;
376
+ const browsed = remote.browsed!;
365
377
  const settled =
366
378
  index === undefined
367
379
  ? []
368
- : settleBrowsed(remote.browsed!, {
369
- namespaces: Object.keys(index.namespaces),
370
- recipes: index.recipes,
371
- });
380
+ : new CatalogMatcher([catalogRepoOfIndex(index, { location: browsed.location })])
381
+ .match(browsed)
382
+ .map((match) => match.ref)
383
+ .filter(isNamedRef);
372
384
 
373
- let found: ParsedRef | undefined = settled.length === 1 ? settled[0] : undefined;
385
+ let found: NamedRef | undefined = settled.length === 1 ? settled[0] : undefined;
374
386
  if (found === undefined) {
375
387
  // The folder may have moved since the release; the key it settled on then
376
388
  // is recorded beside the repository it lives in.
@@ -379,7 +391,7 @@ function settleBrowsedItem(item: WorkItem, index: IndexFile | undefined): void {
379
391
  );
380
392
  if (recorded.length === 1) {
381
393
  const { namespace, name } = splitRecipeKey(recorded[0]!);
382
- found = { namespace, recipe: name };
394
+ found = recipeRef(namespace, name);
383
395
  }
384
396
  }
385
397
 
@@ -392,8 +404,9 @@ function settleBrowsedItem(item: WorkItem, index: IndexFile | undefined): void {
392
404
  );
393
405
  }
394
406
 
395
- const withRange = { ...found, ...(remote.range === undefined ? {} : { range: remote.range }) };
396
- item.ref = { ...pinnedRef(withRange, remote.pins?.[refKey(found)]), repo: item.ref.repo };
407
+ const withRange =
408
+ found.kind === "recipe" && browsed.range !== undefined ? { ...found, range: browsed.range } : found;
409
+ item.ref = withRepoName(pinnedRef(withRange, remote.pins?.[shortKey(found)]), repoOf(item.ref)?.name);
397
410
  }
398
411
 
399
412
  /**
@@ -454,6 +467,11 @@ export async function resolveRefs(
454
467
 
455
468
  const queue: WorkItem[] = requests.map((request) => ({
456
469
  ...request,
470
+ // A recipe ref that carries no range of its own takes the request's.
471
+ ref:
472
+ request.ref.kind === "recipe" && request.ref.range === undefined && request.range !== undefined
473
+ ? { ...request.ref, range: request.range }
474
+ : request.ref,
457
475
  kind: request.kind ?? "subscribes",
458
476
  }));
459
477
 
@@ -463,7 +481,7 @@ export async function resolveRefs(
463
481
  // A ref naming a repository the project has not added never resolves and
464
482
  // never downloads anything; it comes back as a missing repository instead,
465
483
  // so the trust layer can ask about it by name.
466
- const qualifier = item.ref.repo;
484
+ const qualifier = repoOf(item.ref)?.name;
467
485
  if (qualifier !== undefined && !Object.hasOwn(context.repos, qualifier)) {
468
486
  recordMissingRepo(missingRepos, {
469
487
  name: qualifier,
@@ -488,11 +506,11 @@ export async function resolveRefs(
488
506
  });
489
507
  continue;
490
508
  }
491
- item.ref = { ...item.ref, repo: added };
509
+ item.ref = withRepoName(item.ref, added);
492
510
  if (item.remote.browsed !== undefined) settleBrowsedItem(item, context.indexes.get(added));
493
511
  }
494
512
 
495
- if (item.ref.recipe === undefined) {
513
+ if (item.ref.kind === "namespace") {
496
514
  queue.push(...expandNamespace(item, context));
497
515
  continue;
498
516
  }
@@ -677,10 +695,10 @@ function dependencyTargets(
677
695
  } catch {
678
696
  return [];
679
697
  }
680
- const { namespace } = item.ref;
681
- return item.ref.recipe === undefined
698
+ const namespace = namespaceOf(item.ref)!.name;
699
+ return item.ref.kind === "namespace"
682
700
  ? [...resolved.keys()].filter((entry) => entry.startsWith(`${namespace}/`))
683
- : [refKey(item.ref)];
701
+ : [shortKey(item.ref)];
684
702
  }
685
703
 
686
704
  /**
@@ -725,8 +743,9 @@ function recordMissingRepo(into: Map<string, MissingRepo>, missing: MissingRepo)
725
743
  * @param context - The cached indexes and added repositories.
726
744
  */
727
745
  function expandNamespace(item: WorkItem, context: ResolveContext): WorkItem[] {
728
- const { namespace } = item.ref;
729
- const qualifier = item.ref.repo;
746
+ const namespace = namespaceOf(item.ref)!.name;
747
+ const qualifier = repoOf(item.ref)?.name;
748
+ const requestedRange = item.range;
730
749
  const expanded: WorkItem[] = [];
731
750
  const seen = new Set<string>();
732
751
  let namespaceFound = false;
@@ -741,17 +760,15 @@ function expandNamespace(item: WorkItem, context: ResolveContext): WorkItem[] {
741
760
  if (seen.has(key)) continue;
742
761
  seen.add(key);
743
762
  expanded.push({
744
- ref: {
745
- namespace,
746
- recipe: key.slice(namespace.length + 1),
763
+ // A namespace ref cannot carry a range when it is WRITTEN, but a
764
+ // namespace subscription entry can carry one, and a caller builds the
765
+ // request from that entry. When it does, the range applies to every
766
+ // recipe in the namespace; that is how the built-in `core`
767
+ // subscription stays pinned to the running sous version.
768
+ ref: recipeRef(namespace, key.slice(namespace.length + 1), {
747
769
  ...(qualifier === undefined ? {} : { repo: qualifier }),
748
- // A namespace ref cannot carry a range when it is WRITTEN, but a
749
- // namespace subscription entry can carry one, and a caller builds the
750
- // request from that entry. When it does, the range applies to every
751
- // recipe in the namespace; that is how the built-in `core`
752
- // subscription stays pinned to the running sous version.
753
- ...(item.ref.range === undefined ? {} : { range: item.ref.range }),
754
- },
770
+ ...(requestedRange === undefined ? {} : { range: requestedRange }),
771
+ }),
755
772
  requestedBy: item.requestedBy,
756
773
  kind: item.kind,
757
774
  ...(item.prerelease === undefined ? {} : { prerelease: item.prerelease }),
@@ -777,8 +794,8 @@ function resolveRecipeRef(
777
794
  context: ResolveContext,
778
795
  resolved: Map<string, ResolvedRecipe>
779
796
  ): ResolvedRecipe {
780
- const key = refKey(item.ref);
781
- const qualifier = item.ref.repo;
797
+ const key = shortKey(item.ref);
798
+ const qualifier = repoOf(item.ref)?.name;
782
799
 
783
800
  const candidates: Array<{ repo: string; index: IndexFile }> = [];
784
801
  for (const [repoName, index] of context.indexes) {
@@ -802,7 +819,7 @@ function resolveRecipeRef(
802
819
  }
803
820
 
804
821
  const ranges = [...(previous?.ranges ?? [])];
805
- const range = item.ref.range ?? "*";
822
+ const range = rangeOf(item.ref) ?? "*";
806
823
  if (!ranges.some((known) => known.range === range && known.requestedBy === item.requestedBy)) {
807
824
  ranges.push({ range, requestedBy: item.requestedBy });
808
825
  }
@@ -937,7 +954,7 @@ function unknownRefError(
937
954
  // When the ref said WHICH repository, the useful answer is what that
938
955
  // repository does publish: a dependency that names something it has never
939
956
  // heard of is almost always a typo or a recipe that was renamed.
940
- const named = item.ref.repo;
957
+ const named = repoOf(item.ref)?.name;
941
958
  const index = named === undefined ? undefined : context.indexes.get(named);
942
959
  const publishes =
943
960
  index === undefined
@@ -964,8 +981,9 @@ function unknownRefError(
964
981
  * @param repos - The repositories that publish it.
965
982
  */
966
983
  function ambiguousRefError(item: WorkItem, repos: string[]): ConfigError {
967
- const key = refKey(item.ref);
968
- const range = item.ref.range === undefined ? "" : `@${item.ref.range}`;
984
+ const key = shortKey(item.ref);
985
+ const itemRange = rangeOf(item.ref);
986
+ const range = itemRange === undefined ? "" : `@${itemRange}`;
969
987
  const qualified = repos.map((repo) => ` ${repo}:${key}${range}`).join("\n");
970
988
  const asker =
971
989
  item.requestedBy === PROJECT_REQUESTER