@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
@@ -27,23 +27,23 @@ import type { IndexDependency, IndexFile } from "./formats/index-file.js";
27
27
  import type { Lockfile } from "./formats/lockfile.js";
28
28
  import type { ContentKind, RecipeManifest } from "./formats/recipe-manifest.js";
29
29
  import {
30
- isNamedReading,
30
+ CachedIndexLookup,
31
+ RefResolveArguments,
32
+ describeRef,
33
+ isNamedRef,
34
+ locationOf,
35
+ namespaceOf,
31
36
  namespaceOfKey,
32
- parseRef,
33
- refKey,
37
+ repoOf,
38
+ settleDependency,
39
+ shortKey,
40
+ sharedRefResolver,
34
41
  splitRecipeKey,
35
- type ParsedRef,
36
- } from "../refs/parse.js";
37
- import { settleDependency } from "../refs/settle.js";
42
+ type NamedRef,
43
+ type RefKind,
44
+ type SousRef,
45
+ } from "../../services/ref-resolver/index.js";
38
46
  import { declarationFor, type DependencyLists } from "./declarations.js";
39
- import {
40
- describeReference,
41
- findNamespace,
42
- findRecipe,
43
- referenceReposFromIndexes,
44
- type ReferenceContext,
45
- type ReferenceMatch,
46
- } from "../refs/index.js";
47
47
 
48
48
  // --- What the catalog reads ---------------------------------------------------------------------
49
49
 
@@ -305,8 +305,11 @@ export function listRecipes(inputs: CatalogInputs): RecipeListing[] {
305
305
  * @param inputs - The cached indexes, the lockfile and the project's subscriptions.
306
306
  * @param ref - The namespace, optionally qualified with `repo:`.
307
307
  */
308
- export function describeNamespace(inputs: CatalogInputs, ref: string): NamespaceDetail {
309
- const found = resolveNamespaceRef(inputs, ref);
308
+ export async function describeNamespace(
309
+ inputs: CatalogInputs,
310
+ ref: string
311
+ ): Promise<NamespaceDetail> {
312
+ const found = await resolveNamespaceRef(inputs, ref);
310
313
  const subscriptions = new Set(inputs.subscriptions);
311
314
  const declared = found.repo.index.namespaces[found.namespace]!;
312
315
 
@@ -329,8 +332,11 @@ export function describeNamespace(inputs: CatalogInputs, ref: string): Namespace
329
332
  * @param inputs - The cached indexes, the lockfile, the subscriptions and the manifest reader.
330
333
  * @param ref - The recipe, as `namespace/recipe`, a bare recipe name, or either qualified with `repo:`.
331
334
  */
332
- export function describeRecipe(inputs: CatalogInputs, ref: string): RecipeDetail {
333
- const found = resolveRecipeRef(inputs, ref);
335
+ export async function describeRecipe(
336
+ inputs: CatalogInputs,
337
+ ref: string
338
+ ): Promise<RecipeDetail> {
339
+ const found = await resolveRecipeRef(inputs, ref);
334
340
  const subscriptions = new Set(inputs.subscriptions);
335
341
  const listing = recipeListing(found.key, found.repo, inputs, subscriptions);
336
342
  const entry = found.repo.index.recipes[found.key]!;
@@ -412,18 +418,18 @@ export function narrowToInstalled(inputs: CatalogInputs): CatalogInputs {
412
418
  * @param describe - The lookup to run: `describeNamespace` or `describeRecipe`.
413
419
  * @param what - What the ref names, for the error.
414
420
  */
415
- export function describeInstalled<T>(
421
+ export async function describeInstalled<T>(
416
422
  inputs: CatalogInputs,
417
423
  ref: string,
418
- describe: (inputs: CatalogInputs, ref: string) => T,
424
+ describe: (inputs: CatalogInputs, ref: string) => Promise<T>,
419
425
  what: "namespace" | "recipe"
420
- ): T {
426
+ ): Promise<T> {
421
427
  try {
422
- return describe(narrowToInstalled(inputs), ref);
428
+ return await describe(narrowToInstalled(inputs), ref);
423
429
  } catch {
424
430
  // The full lookup either explains the ref better (ambiguous, unknown) or
425
431
  // proves it is published and simply not installed.
426
- describe(inputs, ref);
432
+ await describe(inputs, ref);
427
433
  throw what === "namespace"
428
434
  ? new ConfigError(
429
435
  `This project has installed no recipe from the namespace '${ref}', so there is ` +
@@ -464,35 +470,39 @@ export type ResolvedRecipeRef = {
464
470
  * @param inputs - The cached indexes.
465
471
  * @param ref - The namespace, optionally qualified with `repo:`.
466
472
  */
467
- export function resolveNamespaceRef(inputs: CatalogInputs, ref: string): ResolvedNamespace {
468
- const { parsed, search } = readCommandLineRef(ref);
473
+ export async function resolveNamespaceRef(
474
+ inputs: CatalogInputs,
475
+ ref: string
476
+ ): Promise<ResolvedNamespace> {
477
+ const parsed = readShortRef(ref);
469
478
 
470
- if (parsed?.recipe !== undefined) {
479
+ if (parsed?.kind === "recipe") {
471
480
  throw new ConfigError(
472
- `'${ref}' names the recipe '${parsed.recipe}', not a namespace.\n` +
473
- ` The namespace it belongs to is '${parsed.namespace}'.`
481
+ `'${ref}' names the recipe '${parsed.name}', not a namespace.\n` +
482
+ ` The namespace it belongs to is '${parsed.namespace!.name}'.`
474
483
  );
475
484
  }
476
485
 
477
- const matches = findNamespace(search, referenceContext(inputs));
486
+ const matches = await findIn(inputs, ref, ["namespace"]);
478
487
 
479
488
  if (matches.length === 1) {
480
- return { repo: repoNamed(inputs, matches[0]!.repo!), namespace: matches[0]!.namespace! };
489
+ const match = matches[0]!;
490
+ return { repo: repoNamed(inputs, repoOf(match)!.name!), namespace: namespaceOf(match)!.name };
481
491
  }
482
492
 
483
493
  if (matches.length > 1) throw ambiguousError(ref, "namespace", matches);
484
494
 
485
- const asRecipe = findRecipe(search, referenceContext(inputs));
486
- if (parsed !== undefined && parsed.repo === undefined && asRecipe.length > 0) {
495
+ const asRecipe = await findIn(inputs, ref, ["recipe"]);
496
+ if (parsed !== undefined && repoOf(parsed) === undefined && asRecipe.length > 0) {
487
497
  throw new ConfigError(
488
498
  `No repository this project trusts publishes a namespace called ` +
489
- `'${parsed.namespace}'.\n` +
499
+ `'${parsed.name}'.\n` +
490
500
  ` It is the name of a recipe:\n` +
491
- asRecipe.map((candidate) => ` ${describeReference(candidate)}`).join("\n")
501
+ asRecipe.map((candidate) => ` ${describeRef(candidate)}`).join("\n")
492
502
  );
493
503
  }
494
504
 
495
- throw unknownError(inputs, ref, "namespace", parsed?.repo);
505
+ throw unknownError(inputs, ref, "namespace", parsed === undefined ? undefined : repoOf(parsed)?.name);
496
506
  }
497
507
 
498
508
  /**
@@ -503,35 +513,39 @@ export function resolveNamespaceRef(inputs: CatalogInputs, ref: string): Resolve
503
513
  * @param inputs - The cached indexes.
504
514
  * @param ref - The recipe, as `namespace/recipe`, a bare recipe name, or either qualified with `repo:`.
505
515
  */
506
- export function resolveRecipeRef(inputs: CatalogInputs, ref: string): ResolvedRecipeRef {
507
- const { parsed, search } = readCommandLineRef(ref);
508
- const context = referenceContext(inputs);
516
+ export async function resolveRecipeRef(
517
+ inputs: CatalogInputs,
518
+ ref: string
519
+ ): Promise<ResolvedRecipeRef> {
520
+ const parsed = readShortRef(ref);
509
521
 
510
- const matches = findRecipe(search, context);
522
+ const matches = await findIn(inputs, ref, ["recipe"]);
511
523
 
512
524
  if (matches.length === 1) {
513
525
  const match = matches[0]!;
526
+ const namespace = namespaceOf(match)!.name;
527
+ const name = (match as { name: string }).name;
514
528
  return {
515
- repo: repoNamed(inputs, match.repo!),
516
- key: `${match.namespace}/${match.recipe}`,
517
- namespace: match.namespace!,
518
- name: match.recipe!,
529
+ repo: repoNamed(inputs, repoOf(match)!.name!),
530
+ key: `${namespace}/${name}`,
531
+ namespace,
532
+ name,
519
533
  };
520
534
  }
521
535
 
522
536
  if (matches.length > 1) throw ambiguousError(ref, "recipe", matches);
523
537
 
524
- const asNamespace = findNamespace(search, context);
538
+ const asNamespace = await findIn(inputs, ref, ["namespace"]);
525
539
  if (asNamespace.length > 0) {
526
540
  throw new ConfigError(
527
541
  `No repository this project trusts publishes a recipe called ` +
528
- `'${parsed?.namespace ?? ref}'.\n` +
542
+ `'${parsed === undefined ? ref : namespaceNameOf(parsed)}'.\n` +
529
543
  ` It is the name of a namespace:\n` +
530
- asNamespace.map((candidate) => ` ${describeReference(candidate)}`).join("\n")
544
+ asNamespace.map((candidate) => ` ${describeRef(candidate)}`).join("\n")
531
545
  );
532
546
  }
533
547
 
534
- throw unknownError(inputs, ref, "recipe", parsed?.repo);
548
+ throw unknownError(inputs, ref, "recipe", parsed === undefined ? undefined : repoOf(parsed)?.name);
535
549
  }
536
550
 
537
551
  // --- The pieces ---------------------------------------------------------------------------------
@@ -711,7 +725,7 @@ function dependencyListings(
711
725
  const reading = settleDependency(written, {
712
726
  ...(resolved === undefined ? {} : { recorded: resolved }),
713
727
  });
714
- if (reading !== undefined && isNamedReading(reading)) key = refKey(reading);
728
+ if (reading !== undefined && isNamedRef(reading)) key = shortKey(reading);
715
729
  } catch {
716
730
  key = written;
717
731
  }
@@ -733,45 +747,57 @@ function repoNamed(inputs: CatalogInputs, name: string): CatalogRepo {
733
747
  return inputs.repos.find((repo) => repo.name === name)!;
734
748
  }
735
749
 
736
- /** This catalog's repositories, in the shape a reference searches. */
737
- function referenceContext(inputs: CatalogInputs): ReferenceContext {
738
- return {
739
- repos: referenceReposFromIndexes(
740
- inputs.repos.map((repo) => repo.name),
741
- new Map(inputs.repos.map((repo) => [repo.name, repo.index])),
742
- Object.fromEntries(inputs.repos.map((repo) => [repo.name, repo.url]))
743
- ),
744
- };
750
+ /**
751
+ * Every ref in this catalog's repositories that a ref written on the command
752
+ * line names, of the kinds given, best match first.
753
+ *
754
+ * @param inputs - The cached indexes.
755
+ * @param ref - The ref as typed.
756
+ * @param kinds - The kinds of ref the caller accepts.
757
+ */
758
+ async function findIn(
759
+ inputs: CatalogInputs,
760
+ ref: string,
761
+ kinds: readonly RefKind[]
762
+ ): Promise<SousRef[]> {
763
+ const lookup = new CachedIndexLookup(
764
+ new Map(inputs.repos.map((repo) => [repo.name, repo.index])),
765
+ {
766
+ order: inputs.repos.map((repo) => repo.name),
767
+ urls: Object.fromEntries(inputs.repos.map((repo) => [repo.name, repo.url])),
768
+ }
769
+ );
770
+ const result = await sharedRefResolver().resolve(new RefResolveArguments({ input: ref, lookup, kinds }));
771
+ return result.refs;
745
772
  }
746
773
 
747
774
  /**
748
- * A ref from the command line, checked by the one parser, and the spelling to
749
- * search for: a short ref written back without its version range (the range
750
- * says which version to use, never which thing is meant), or a location
751
- * exactly as written, which the search settles through the repository's index.
775
+ * The namespace or recipe a ref from the command line names by its short form,
776
+ * checked by the one parser: undefined when the ref names a location instead.
752
777
  *
753
778
  * @param ref - The ref as typed.
754
- * @returns The short reading, when the ref was short, and the search term.
755
779
  */
756
- function readCommandLineRef(ref: string): { parsed?: ParsedRef; search: string } {
757
- const readings = parseRef(ref);
758
- const first = readings[0]!;
759
- if (first.location !== undefined || !isNamedReading(first)) return { search: ref.trim() };
760
- return {
761
- parsed: first,
762
- search: first.repo === undefined ? refKey(first) : `${first.repo}:${refKey(first)}`,
763
- };
780
+ function readShortRef(ref: string): NamedRef | undefined {
781
+ const { refs } = sharedRefResolver().parse(ref);
782
+ return refs.find(
783
+ (candidate): candidate is NamedRef => isNamedRef(candidate) && locationOf(candidate) === undefined
784
+ );
785
+ }
786
+
787
+ /** The name a short ref was written with: its namespace for a namespace, its recipe name otherwise. */
788
+ function namespaceNameOf(ref: NamedRef): string {
789
+ return ref.kind === "namespace" ? ref.name : ref.namespace!.name;
764
790
  }
765
791
 
766
792
  /** The error a ref that could have meant several things raises. */
767
793
  function ambiguousError(
768
794
  ref: string,
769
795
  what: "namespace" | "recipe",
770
- candidates: ReferenceMatch[]
796
+ candidates: SousRef[]
771
797
  ): ConfigError {
772
798
  return new ConfigError(
773
799
  `'${ref}' names a ${what} in more than one repository this project trusts:\n` +
774
- candidates.map((candidate) => ` ${describeReference(candidate)}`).join("\n") +
800
+ candidates.map((candidate) => ` ${describeRef(candidate)}`).join("\n") +
775
801
  `\n Name the repository as well, as 'repository:${ref}', to say which one you mean.`
776
802
  );
777
803
  }
@@ -14,9 +14,16 @@
14
14
 
15
15
  import type { DependencyKind } from "./formats/common.js";
16
16
  import type { IndexVersion } from "./formats/index-file.js";
17
- import { isNamedReading, namespaceOfKey, parseRef, refKey } from "../refs/parse.js";
18
- import { RefSource } from "../refs/scopes.js";
19
- import { settleDependency, type RecordedDependencies } from "../refs/settle.js";
17
+ import {
18
+ RefSource,
19
+ isNamedRef,
20
+ locationOf,
21
+ namespaceOfKey,
22
+ settleDependency,
23
+ shortKey,
24
+ sharedRefResolver,
25
+ type RecordedDependencies,
26
+ } from "../../services/ref-resolver/index.js";
20
27
 
21
28
  /** A manifest's two dependency lists, each entry exactly as it was written. */
22
29
  export type DependencyLists = {
@@ -112,10 +119,12 @@ export function declarationFor(
112
119
  // validated; here it simply covers nothing.
113
120
  continue;
114
121
  }
115
- if (reading === undefined || !isNamedReading(reading)) continue;
122
+ if (reading === undefined || !isNamedRef(reading)) continue;
116
123
 
117
- const named = reading.recipe !== undefined;
118
- const covers = named ? refKey(reading) === key : namespaceOfKey(key) === reading.namespace;
124
+ const named = reading.kind === "recipe";
125
+ const covers = named
126
+ ? shortKey(reading) === key
127
+ : namespaceOfKey(key) === reading.name;
119
128
  if (covers) gathered = foldDeclaration(gathered, { written, kind, named });
120
129
  }
121
130
  }
@@ -152,12 +161,13 @@ export function indexDependencyLists(
152
161
  */
153
162
  export function describeDeclaration(declared: string): string {
154
163
  try {
155
- const readings = parseRef(declared, RefSource.Manifest);
156
- const only = readings[0]!;
157
- if (readings.length === 1 && isNamedReading(only) && only.recipe === undefined) {
158
- return only.location === undefined
159
- ? `the whole '${only.namespace}' namespace`
160
- : `the whole '${only.namespace}' namespace of ${only.location.identity}`;
164
+ const { refs } = sharedRefResolver().parse(declared, RefSource.Manifest);
165
+ const only = refs[0]!;
166
+ if (refs.length === 1 && only.kind === "namespace") {
167
+ const location = locationOf(only);
168
+ return location === undefined
169
+ ? `the whole '${only.name}' namespace`
170
+ : `the whole '${only.name}' namespace of ${location.identity}`;
161
171
  }
162
172
  } catch {
163
173
  // Shown as it was written; the reader sees the bad entry.
@@ -33,7 +33,7 @@ import {
33
33
  stableJsonStringify,
34
34
  } from "./common.js";
35
35
  import { publishedVariableDefinitionSchema } from "./variable-definition.js";
36
- import { namespaceOfKey } from "../../refs/parse.js";
36
+ import { namespaceOfKey } from "../../../services/ref-resolver/index.js";
37
37
 
38
38
  /**
39
39
  * One dependency of one published version, as the release resolved it.
@@ -22,7 +22,7 @@
22
22
  *
23
23
  * Every other location form works too (an HTTPS or SSH URL, a browser URL); a
24
24
  * short name such as `sous-recipes:` is a consuming project's own label, so it
25
- * never appears in a published manifest. See `parseRef` with
25
+ * never appears in a published manifest. See `RefResolverService.parse` with
26
26
  * `RefSource.Manifest`.
27
27
  */
28
28
 
@@ -37,8 +37,7 @@ import {
37
37
  semverVersionSchema,
38
38
  submissionsSchema,
39
39
  } from "./common.js";
40
- import { parseRef } from "../../refs/parse.js";
41
- import { RefSource } from "../../refs/scopes.js";
40
+ import { RefSource, sharedRefResolver } from "../../../services/ref-resolver/index.js";
42
41
  import { variableDefinitionSchema } from "./variable-definition.js";
43
42
 
44
43
  // Variable definitions live in their own module, so the index can read them
@@ -56,7 +55,7 @@ export * from "./variable-definition.js";
56
55
  */
57
56
  const dependencyRefSchema = z.string().superRefine((value, ctx) => {
58
57
  try {
59
- parseRef(value, RefSource.Manifest);
58
+ sharedRefResolver().parse(value, RefSource.Manifest);
60
59
  } catch (error) {
61
60
  ctx.addIssue({ code: "custom", message: (error as Error).message });
62
61
  }
@@ -34,7 +34,7 @@ import type { ResolvedRecipe } from "./resolver.js";
34
34
  import type { RecipeStoreLike, StoreKey } from "./store/contract.js";
35
35
  import { builtInProviders, requireProvider } from "./providers/index.js";
36
36
  import { repoIdentity } from "./identity.js";
37
- import { splitRecipeKey } from "../refs/parse.js";
37
+ import { splitRecipeKey } from "../../services/ref-resolver/index.js";
38
38
  import type { ProviderOptions, RepoProvider } from "./providers/provider.js";
39
39
 
40
40
  /** What the lockfile needs to know about a repository. */
@@ -24,15 +24,19 @@
24
24
  * can tell.
25
25
  *
26
26
  * A project with no lockfile entries gets no resolver at all, so a project that
27
- * uses no repositories behaves exactly as it did before: `~` in an include line
28
- * means an alias and nothing else.
27
+ * uses no repositories behaves exactly as it did before: a `~` reference in an
28
+ * include line is just a relative path.
29
29
  */
30
30
 
31
31
  import semver from "semver";
32
32
  import type { Settings } from "../settings.js";
33
33
  import { resolveStoreRoot } from "../sous-home.js";
34
- import { RefSource } from "../refs/scopes.js";
35
- import { namespaceOfKey, parseShortRef, refKey } from "../refs/parse.js";
34
+ import {
35
+ RefSource,
36
+ namespaceOfKey,
37
+ parseNamedRef,
38
+ shortKey,
39
+ } from "../../services/ref-resolver/index.js";
36
40
  import {
37
41
  StaticNamespaceResolver,
38
42
  type DroppedRecipe,
@@ -64,7 +68,8 @@ export type ProjectNamespaceResolverOptions = {
64
68
  * locks no recipes at all.
65
69
  *
66
70
  * createProjectNamespaceResolver({ sousDir, settings })
67
- * // -> resolves "@~workflow/task-files/_partials/resume.md" to the pinned
71
+ * // -> resolves "@~workflow/task-files/_partials/resume.md" (or a glob such as
72
+ * // "@~*" + "/*" + "/memories/*.md") to the pinned
68
73
  * // recipe directory, when the project (or the including recipe) may address it
69
74
  *
70
75
  * @param options - The project's `.sous/` directory, its config and the environment.
@@ -82,10 +87,12 @@ export function createProjectNamespaceResolver(
82
87
  if (locked.length === 0) return undefined;
83
88
 
84
89
  const recipes: Record<string, string> = {};
90
+ const repos: Record<string, string> = {};
85
91
  const dependencies: Record<string, string[]> = {};
86
92
 
87
93
  for (const recipe of locked) {
88
94
  recipes[recipe.key] = recipe.dir;
95
+ repos[recipe.key] = recipe.repo;
89
96
 
90
97
  // Every recipe holding this one declared it, in `depends` or `subscribes`,
91
98
  // and the lockfile records the key that declaration resolved to.
@@ -98,6 +105,7 @@ export function createProjectNamespaceResolver(
98
105
  // lockfile pins, and those are exactly the recipes this resolver knows.
99
106
  return new StaticNamespaceResolver({
100
107
  recipes,
108
+ repos,
101
109
  dependencies,
102
110
  explainMissing: (recipe) => explainUnpinnedRecipe(recipe, options),
103
111
  });
@@ -131,7 +139,7 @@ export function explainUnpinnedRecipe(
131
139
  if (entry?.enabled !== false) continue;
132
140
  let ref: string;
133
141
  try {
134
- ref = refKey(parseShortRef(key, RefSource.Config));
142
+ ref = shortKey(parseNamedRef(key, RefSource.Config));
135
143
  } catch {
136
144
  // The config schema already refuses a key that is not a ref.
137
145
  continue;
@@ -0,0 +1,123 @@
1
+ /**
2
+ * What an include line may name: the recipes the lockfile pins.
3
+ *
4
+ * A `RefLookup` over the pinned recipes, answering a `recipeFile` reading (the
5
+ * namespace and recipe names, and the path inside the recipe) with every pinned
6
+ * recipe whose names match it. Names are compared as the ref service compares
7
+ * every name: exact spelling first, ignoring case as the fallback, and a name
8
+ * may be a glob (`*`, `**`, `?`, `[..]`, `{a,b}`). The path inside the recipe
9
+ * is carried through as written; expanding it into files is the include
10
+ * resolver's job, because only it knows which directory each recipe lives in.
11
+ */
12
+
13
+ import {
14
+ compareBytewise,
15
+ matchName,
16
+ repoOf,
17
+ type NameMatch,
18
+ type RefMatch,
19
+ type SousRef,
20
+ type SyncRefLookup,
21
+ } from "../../services/ref-resolver/index.js";
22
+
23
+ /** One pinned recipe, as far as a lookup needs to know it. */
24
+ export type RecipeFileEntry = {
25
+ /** The recipe's namespace. */
26
+ namespace: string;
27
+ /** The recipe's name. */
28
+ name: string;
29
+ /** The short name of the repository it came from, when known. */
30
+ repo?: string;
31
+ };
32
+
33
+ /** The weakest of two matches: a name that matched only ignoring case spoils an exact one. */
34
+ function combine(left: NameMatch, right: NameMatch): NameMatch {
35
+ if (left === undefined || right === undefined) return undefined;
36
+ return left === "exact" && right === "exact" ? "exact" : "folded";
37
+ }
38
+
39
+ /** Answers which pinned recipes an include reading names. */
40
+ export class LockedRecipeFileLookup implements SyncRefLookup {
41
+ /**
42
+ * @param entries - Every recipe the lockfile pins.
43
+ */
44
+ constructor(private readonly entries: readonly RecipeFileEntry[]) {}
45
+
46
+ /** The distinct namespaces of the pinned recipes, sorted. */
47
+ namespaces(): string[] {
48
+ return [...new Set(this.entries.map((entry) => entry.namespace))].sort(compareBytewise);
49
+ }
50
+
51
+ /**
52
+ * The pinned recipes of the namespaces a written namespace matches, sorted
53
+ * by key: those whose spelling matches exactly, or, when none does, those
54
+ * that differ only in case.
55
+ *
56
+ * @param written - The namespace as written, possibly a glob.
57
+ */
58
+ recipesInNamespace(written: string): string[] {
59
+ const matched = this.entries
60
+ .map((entry) => ({ entry, match: matchName(written, entry.namespace) }))
61
+ .filter((hit) => hit.match !== undefined);
62
+ const exact = matched.filter((hit) => hit.match === "exact");
63
+ return (exact.length > 0 ? exact : matched)
64
+ .map((hit) => `${hit.entry.namespace}/${hit.entry.name}`)
65
+ .sort(compareBytewise);
66
+ }
67
+
68
+ /**
69
+ * True when a written namespace matches a pinned one in some spelling.
70
+ *
71
+ * @param written - The namespace as written, possibly a glob.
72
+ */
73
+ knowsNamespace(written: string): boolean {
74
+ return this.entries.some((entry) => matchName(written, entry.namespace) !== undefined);
75
+ }
76
+
77
+ findSync(candidate: SousRef): RefMatch[] {
78
+ if (candidate.kind !== "recipeFile") return [];
79
+ const written = candidate.recipe;
80
+ const namespaceName = written.namespace?.name;
81
+ if (namespaceName === undefined) return [];
82
+ const repoName = repoOf(candidate)?.name;
83
+
84
+ const found: RefMatch[] = [];
85
+ for (const entry of this.entries) {
86
+ let match = combine(
87
+ matchName(namespaceName, entry.namespace),
88
+ matchName(written.name, entry.name)
89
+ );
90
+ if (repoName !== undefined) {
91
+ match = combine(
92
+ match,
93
+ entry.repo === undefined ? undefined : matchName(repoName, entry.repo)
94
+ );
95
+ }
96
+ if (match === undefined) continue;
97
+ found.push({
98
+ ref: {
99
+ kind: "recipeFile",
100
+ path: candidate.path,
101
+ recipe: {
102
+ kind: "recipe",
103
+ name: entry.name,
104
+ namespace: { kind: "namespace", name: entry.namespace },
105
+ },
106
+ ...(candidate.glob === undefined ? {} : { glob: true as const }),
107
+ ...(candidate.vars === undefined ? {} : { vars: candidate.vars }),
108
+ },
109
+ exactSpelling: match === "exact",
110
+ });
111
+ }
112
+ return found.sort((left, right) => compareBytewise(keyOf(left.ref), keyOf(right.ref)));
113
+ }
114
+
115
+ async find(candidate: SousRef): Promise<RefMatch[]> {
116
+ return this.findSync(candidate);
117
+ }
118
+ }
119
+
120
+ /** The `namespace/recipe` key of a recipe file ref, for ordering. */
121
+ function keyOf(ref: SousRef): string {
122
+ return ref.kind === "recipeFile" ? `${ref.recipe.namespace?.name}/${ref.recipe.name}` : "";
123
+ }
@@ -33,7 +33,7 @@ import {
33
33
  } from "./load-manifest.js";
34
34
  import { readEffectiveLinks } from "./links.js";
35
35
  import { identitySegments } from "./identity.js";
36
- import { splitRecipeKey } from "../refs/parse.js";
36
+ import { splitRecipeKey } from "../../services/ref-resolver/index.js";
37
37
  import { PROJECT_HOLDER } from "./formats/lockfile.js";
38
38
 
39
39
  /** One locked recipe, together with the directory its files are read from. */
@@ -0,0 +1,33 @@
1
+ /**
2
+ * The built-in `#memories` view: every memory an active recipe publishes, at
3
+ * `#memories/<namespace>/<recipe>/<path>`. See `recipe-memories.ts` for what is
4
+ * listed and in which order.
5
+ */
6
+
7
+ import { makeInjectable } from "../../services/ref-resolver/injectable.js";
8
+ import type { HashName, HashViewContext, ViewFile } from "../../services/ref-resolver/hash-names.js";
9
+ import { listMemories } from "./recipe-memories.js";
10
+
11
+ /** The `#memories` view. */
12
+ export class MemoriesHashName implements HashName {
13
+ readonly name = "memories";
14
+ readonly registeredBy = "sous";
15
+ readonly description =
16
+ "the memories of every active recipe, listed in dependency order as #memories/<namespace>/<recipe>/<path>.";
17
+
18
+ bases(): string[] {
19
+ return [];
20
+ }
21
+
22
+ view(context: HashViewContext): ViewFile[] {
23
+ const memories = context.settings.recipes?.memories;
24
+ return listMemories({
25
+ sousDir: context.sousDir,
26
+ ...(context.env === undefined ? {} : { env: context.env }),
27
+ first: memories?.first,
28
+ exclude: memories?.exclude,
29
+ }).map((memory) => ({ path: memory.path, file: memory.file }));
30
+ }
31
+ }
32
+
33
+ makeInjectable(MemoriesHashName);