@sous-io/sous 0.2.32 → 0.2.34

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 (111) 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-variables.md +16 -0
  10. package/docs/markdown/repositories.md +8 -4
  11. package/package.json +2 -1
  12. package/recipes/core/sous-skills/skills/about-liquid-templates/SKILL.tpl.md +33 -20
  13. package/recipes/core/sous-skills/skills/about-sous-configuration/SKILL.tpl.md +4 -0
  14. package/recipes/core/sous-skills/skills/about-sous-recipes/SKILL.tpl.md +21 -9
  15. package/recipes/core/sous-skills/sous.recipe.yaml +1 -1
  16. package/sous.config.schema.json +27 -15
  17. package/src/commands/lock/rebuild.ts +3 -6
  18. package/src/commands/namespace/show.ts +2 -2
  19. package/src/commands/recipe/show.ts +26 -9
  20. package/src/commands/repo/contribute.ts +32 -23
  21. package/src/commands/repo/gc.ts +1 -1
  22. package/src/commands/repo/remove.ts +1 -1
  23. package/src/commands/repo/search.ts +1 -1
  24. package/src/commands/vars/ask.ts +75 -54
  25. package/src/lib/build-service.ts +93 -15
  26. package/src/lib/config-schema.ts +59 -19
  27. package/src/lib/include-resolver.ts +307 -69
  28. package/src/lib/markdown-compiler.ts +289 -82
  29. package/src/lib/project-scaffold/templates.ts +19 -11
  30. package/src/lib/repos/catalog-inputs.ts +1 -1
  31. package/src/lib/repos/catalog.ts +98 -72
  32. package/src/lib/repos/declarations.ts +22 -12
  33. package/src/lib/repos/formats/index-file.ts +1 -1
  34. package/src/lib/repos/formats/recipe-manifest.ts +3 -4
  35. package/src/lib/repos/lock-service.ts +1 -1
  36. package/src/lib/repos/locked-namespace-resolver.ts +14 -6
  37. package/src/lib/repos/locked-recipe-lookup.ts +123 -0
  38. package/src/lib/repos/locked-recipes.ts +1 -1
  39. package/src/lib/repos/memories-hash-name.ts +33 -0
  40. package/src/lib/repos/namespace-resolver.ts +198 -119
  41. package/src/lib/repos/recipe-config-layers.ts +29 -1
  42. package/src/lib/repos/recipe-key-matcher.ts +62 -0
  43. package/src/lib/repos/recipe-memories.ts +175 -0
  44. package/src/lib/repos/recipe-targets.ts +32 -28
  45. package/src/lib/repos/ref-search.ts +1 -1
  46. package/src/lib/repos/release/index-builder.ts +23 -15
  47. package/src/lib/repos/release/plan.ts +14 -7
  48. package/src/lib/repos/release/settle.ts +38 -121
  49. package/src/lib/repos/release/submit-checkout.ts +42 -23
  50. package/src/lib/repos/release/tags.ts +1 -1
  51. package/src/lib/repos/resolver.ts +90 -72
  52. package/src/lib/repos/subscription-service.ts +192 -170
  53. package/src/lib/settings.ts +37 -33
  54. package/src/lib/vars/answers.ts +9 -1
  55. package/src/lib/vars/ask.ts +3 -3
  56. package/src/lib/vars/display.ts +1 -3
  57. package/src/lib/vars/index.ts +1 -0
  58. package/src/lib/vars/mappings.ts +17 -9
  59. package/src/lib/vars/secrets.ts +131 -0
  60. package/src/services/ref-resolver/container.ts +107 -0
  61. package/src/services/ref-resolver/dependency.ts +60 -0
  62. package/src/services/ref-resolver/format.ts +197 -0
  63. package/src/services/ref-resolver/glob.ts +115 -0
  64. package/src/services/ref-resolver/hash-names.ts +155 -0
  65. package/src/services/ref-resolver/index.ts +55 -0
  66. package/src/services/ref-resolver/injectable.ts +36 -0
  67. package/src/services/ref-resolver/location.ts +62 -0
  68. package/src/services/ref-resolver/lookups/cached-index-lookup.ts +53 -0
  69. package/src/services/ref-resolver/lookups/catalog-lookup.ts +23 -0
  70. package/src/services/ref-resolver/lookups/catalog-matcher.ts +261 -0
  71. package/src/services/ref-resolver/lookups/chained-lookup.ts +27 -0
  72. package/src/services/ref-resolver/lookups/env-var-lookup.ts +65 -0
  73. package/src/services/ref-resolver/lookups/fetched-index-lookup.ts +150 -0
  74. package/src/services/ref-resolver/lookups/recorded-dependency-lookup.ts +62 -0
  75. package/src/services/ref-resolver/lookups/ref-lookup.ts +54 -0
  76. package/src/services/ref-resolver/lookups/variable-lookup.ts +123 -0
  77. package/src/services/ref-resolver/named-refs.ts +144 -0
  78. package/src/services/ref-resolver/parser/env-var-splitter.ts +27 -0
  79. package/src/services/ref-resolver/parser/location-splitter.ts +239 -0
  80. package/src/services/ref-resolver/parser/name-path-splitter.ts +200 -0
  81. package/src/services/ref-resolver/parser/partial-ref.ts +84 -0
  82. package/src/services/ref-resolver/parser/query-splitter.ts +61 -0
  83. package/src/services/ref-resolver/parser/range-splitter.ts +52 -0
  84. package/src/services/ref-resolver/parser/ref-parser.ts +115 -0
  85. package/src/services/ref-resolver/parser/repo-qualifier-splitter.ts +51 -0
  86. package/src/services/ref-resolver/parts.ts +115 -0
  87. package/src/services/ref-resolver/pruners/command-line-pruner.ts +47 -0
  88. package/src/services/ref-resolver/pruners/include-pruner.ts +66 -0
  89. package/src/services/ref-resolver/pruners/manifest-pruner.ts +130 -0
  90. package/src/services/ref-resolver/pruners/ref-pruner.ts +135 -0
  91. package/src/services/ref-resolver/pruners/rule-helpers.ts +44 -0
  92. package/src/services/ref-resolver/pruners/stored-key-pruner.ts +136 -0
  93. package/src/services/ref-resolver/ref-helpers.ts +62 -0
  94. package/src/services/ref-resolver/ref-picker-service.ts +204 -0
  95. package/src/{lib/repos/reference-report.ts → services/ref-resolver/ref-report.ts} +2 -40
  96. package/src/services/ref-resolver/ref-resolve-arguments.ts +47 -0
  97. package/src/services/ref-resolver/ref-resolve-result.ts +36 -0
  98. package/src/services/ref-resolver/ref-resolver-service.ts +323 -0
  99. package/src/services/ref-resolver/source.ts +48 -0
  100. package/src/services/ref-resolver/tokens.ts +25 -0
  101. package/src/services/ref-resolver/types.ts +155 -0
  102. package/src/templating/init-liquid-engine.ts +33 -18
  103. package/src/templating/lib/secret-scope.ts +41 -0
  104. package/src/templating/tags/exportScalarVarsJs.ts +5 -2
  105. package/src/templating/tags/showVars.ts +8 -2
  106. package/src/lib/refs/find.ts +0 -770
  107. package/src/lib/refs/index.ts +0 -14
  108. package/src/lib/refs/parse.ts +0 -844
  109. package/src/lib/refs/pick.ts +0 -147
  110. package/src/lib/refs/scopes.ts +0 -95
  111. package/src/lib/refs/settle.ts +0 -219
@@ -1,147 +0,0 @@
1
- /**
2
- * Settling on one of the things a reference could have meant.
3
- *
4
- * Finding is separate from choosing on purpose: `findReference` is pure and
5
- * tells the caller everything a word could have meant, and this is the one
6
- * place that decides which of them the run proceeds with. Every command that
7
- * takes a reference goes through it, so a word that means two things behaves
8
- * the same everywhere:
9
- *
10
- * - one match proceeds, and what it resolved to is written as the facts about
11
- * it followed by one sentence (`formatResolvedReference`), so the reader
12
- * sees what the word they typed actually meant;
13
- * - several matches are offered as a list to choose from;
14
- * - `--accept-first` takes the first one in the documented listing order;
15
- * - a run with no terminal fails, naming the question it could not ask and
16
- * the flag that would have answered it (`src/lib/interactive.ts` holds that
17
- * rule, and every prompt in sous is gated by it).
18
- */
19
-
20
- import { ConfigError } from "../errors.js";
21
- import { nonInteractiveError } from "../interactive.js";
22
- import { formatParagraph, log } from "../../utils/formatting.js";
23
- import { askChoice } from "../../utils/prompts.js";
24
- import {
25
- formatResolvedReference,
26
- resolvedReferenceFacts,
27
- } from "../repos/reference-report.js";
28
- import { describeReference, type ReferenceMatch } from "./find.js";
29
-
30
- /** The flag that answers "which one did you mean?" ahead of time. */
31
- export const ACCEPT_FIRST_FLAG = "--accept-first";
32
-
33
- /** How `pickReference` should behave. */
34
- export type PickReferenceOptions = {
35
- /** The reference exactly as it was written, for every message. */
36
- search: string;
37
- /** Whether a question may be asked. A run that may not fails instead. */
38
- interactive: boolean;
39
- /** Take the first match rather than asking, because `--accept-first` was passed. */
40
- acceptFirst?: boolean;
41
- /** The question to ask when there is more than one match. */
42
- prompt?: string;
43
- /**
44
- * Write the facts about what the reference resolved to. On by default; a
45
- * caller that reports the resolution itself turns it off, and is still told
46
- * when `--accept-first` settled an ambiguous word.
47
- */
48
- announce?: boolean;
49
- /** Extra lines for the error a reference that matched nothing raises. */
50
- details?: string[];
51
- /** Where a resolution is reported. Defaults to the console. */
52
- write?: (message: string) => void;
53
- /** How the choice is asked. Defaults to the shared choice prompt. */
54
- choose?: (message: string, matches: ReferenceMatch[]) => Promise<ReferenceMatch>;
55
- };
56
-
57
- /**
58
- * The one match a reference proceeds with.
59
- *
60
- * @param matches - What the reference could have meant, in listing order.
61
- * @param options - What was searched for, and whether a question may be asked.
62
- * @returns The match the run proceeds with.
63
- */
64
- export async function pickReference(
65
- matches: ReferenceMatch[],
66
- options: PickReferenceOptions
67
- ): Promise<ReferenceMatch> {
68
- const write = options.write ?? ((message: string) => log(message));
69
-
70
- if (matches.length === 0) {
71
- throw new ConfigError(
72
- [`Nothing called '${options.search}' was found.`, ...(options.details ?? [])].join("\n")
73
- );
74
- }
75
-
76
- const first = matches[0]!;
77
-
78
- if (matches.length === 1) {
79
- if (options.announce !== false) announce(write, first, options.search);
80
- return first;
81
- }
82
-
83
- if (options.acceptFirst === true) {
84
- const reason =
85
- `'${options.search}' named ${matches.length} things, and ` +
86
- `'${ACCEPT_FIRST_FLAG}' was passed, so the first one listed is being used.`;
87
-
88
- // A caller that reports the resolution itself still has to be told that the
89
- // word was ambiguous, so the sentence is written even when the facts are not.
90
- if (options.announce === false) {
91
- for (const line of formatParagraph(reason)) write(line);
92
- } else {
93
- announce(write, first, options.search, reason);
94
- }
95
- return first;
96
- }
97
-
98
- if (!options.interactive) {
99
- // The example is a spelling that says more than what was typed, so a
100
- // reference that is already its own fully qualified name is not offered
101
- // back unchanged.
102
- const example =
103
- matches.find((match) => match.key !== options.search)?.key ?? first.key;
104
-
105
- throw nonInteractiveError({
106
- prompt: `which '${options.search}' you meant`,
107
- remedy:
108
- `write the full reference (for example '${example}'), or pass ` +
109
- `'${ACCEPT_FIRST_FLAG}' to take the first candidate listed above.`,
110
- details: [
111
- `'${options.search}' matched ${matches.length} things:`,
112
- ...matches.map((match) => ` ${describeReference(match)}`),
113
- ],
114
- });
115
- }
116
-
117
- const choose =
118
- options.choose ??
119
- ((message: string, offered: ReferenceMatch[]) =>
120
- askChoice(
121
- message,
122
- offered.map((match) => ({ name: describeReference(match), value: match }))
123
- ));
124
-
125
- return choose(options.prompt ?? `Which '${options.search}' did you mean?`, matches);
126
- }
127
-
128
- /**
129
- * Writes what a reference resolved to, as the key and value list every other
130
- * set of facts in the CLI is written as, followed by one sentence saying why
131
- * that candidate won.
132
- *
133
- * @param write - Where the report goes.
134
- * @param match - The match the run proceeds with.
135
- * @param search - The reference exactly as it was written.
136
- * @param reason - The closing sentence, when it was not simply the only match.
137
- */
138
- function announce(
139
- write: (message: string) => void,
140
- match: ReferenceMatch,
141
- search: string,
142
- reason?: string
143
- ): void {
144
- for (const line of formatResolvedReference(resolvedReferenceFacts(match, search, reason))) {
145
- write(line);
146
- }
147
- }
@@ -1,95 +0,0 @@
1
- /**
2
- * The kinds of thing a reference on the command line can name.
3
- *
4
- * Every sous command that takes a `<ref>` or a `[NAME]` argument is naming one
5
- * of these five things, and each command accepts only the kinds that make sense
6
- * for it: `sous subscribe` accepts a namespace or a recipe, `sous vars ask`
7
- * accepts all five. The scope list a command passes to `findReference` is
8
- * therefore part of that command's contract, and it is the only thing that
9
- * differs between one command's resolution and another's.
10
- */
11
-
12
- /** One kind of thing a reference can name. */
13
- export enum SousScope {
14
- /** A repository this project trusts, named by its short name. */
15
- Repository = "repository",
16
- /** A namespace published by a repository. */
17
- Namespace = "namespace",
18
- /** A recipe published in a namespace. */
19
- Recipe = "recipe",
20
- /** A variable declared by a recipe, named as the recipe's author named it. */
21
- VariableName = "variableName",
22
- /** An environment variable name that answers a variable. */
23
- EnvVarName = "envVarName",
24
- }
25
-
26
- /**
27
- * Where a ref was written. `parseRef` reads every form wherever a ref comes
28
- * from, and the place decides which of those forms it allows: a refused form
29
- * is an error saying what to write there instead.
30
- */
31
- export enum RefSource {
32
- /**
33
- * A ref typed on the command line. Every form is allowed: a bare name, a
34
- * namespace and recipe, a `repo:` qualifier, a version range, a
35
- * provider-scheme locator, an HTTPS, SSH or scheme-less URL, and a browser
36
- * URL copied from a host's file view. Names may be written in any case.
37
- */
38
- CommandLine = "commandLine",
39
- /**
40
- * A subscription key in a config layer. Only the stored form is allowed:
41
- * `namespace` or `namespace/recipe`, lowercase. The repository a
42
- * subscription resolves into is recorded in the lockfile, and its range in
43
- * the entry's own `range` field.
44
- */
45
- Config = "config",
46
- /**
47
- * An entry of a recipe manifest's `depends` or `subscribes` list. A bare ref
48
- * names a recipe in the same repository; every locator and URL form names
49
- * one in another repository. A `repo:` qualifier is refused, because it is
50
- * one project's private name for a repository, and so is a local path.
51
- */
52
- Manifest = "manifest",
53
- /**
54
- * A key sous wrote itself: the lockfile, the index and the store. Only the
55
- * canonical `namespace` or `namespace/recipe` is allowed.
56
- */
57
- Lockfile = "lockfile",
58
- }
59
-
60
- /**
61
- * Every scope, in the order matches of equal specificity are listed in. It is
62
- * the order the one-word ref search has always used (a whole namespace before
63
- * the recipes inside it), widened to the other three kinds: the broadest thing
64
- * a word could have meant is offered first, and the environment variable names
65
- * (the least likely reading of a plain word) come last.
66
- */
67
- export const SCOPE_ORDER: readonly SousScope[] = [
68
- SousScope.Repository,
69
- SousScope.Namespace,
70
- SousScope.Recipe,
71
- SousScope.VariableName,
72
- SousScope.EnvVarName,
73
- ] as const;
74
-
75
- /** Every scope, for a command that accepts anything a reference can name. */
76
- export const ALL_SCOPES: readonly SousScope[] = SCOPE_ORDER;
77
-
78
- /** What each scope is called in a sentence. */
79
- export const SCOPE_LABELS: Record<SousScope, string> = {
80
- [SousScope.Repository]: "repository",
81
- [SousScope.Namespace]: "namespace",
82
- [SousScope.Recipe]: "recipe",
83
- [SousScope.VariableName]: "variable",
84
- [SousScope.EnvVarName]: "environment variable",
85
- };
86
-
87
- /**
88
- * Where a scope sits in the listing order.
89
- *
90
- * @param scope - The scope to rank.
91
- */
92
- export function scopeRank(scope: SousScope): number {
93
- const rank = SCOPE_ORDER.indexOf(scope);
94
- return rank === -1 ? SCOPE_ORDER.length : rank;
95
- }
@@ -1,219 +0,0 @@
1
- /**
2
- * Settling what a ref means, once the index that can say is at hand.
3
- *
4
- * `parseRef` returns every reading of a written ref. Some readings are settled
5
- * the moment they are read (`workflow/alpha`), and some need the index of the
6
- * repository they point at: a browser URL names a folder, and only the `path`
7
- * each recipe's index entry records says which recipe that folder is; a GitLab
8
- * URL with nested groups can be read as several repositories, and only their
9
- * indexes say which one publishes what was named.
10
- *
11
- * Names are matched the same way everywhere: the exact spelling first, then
12
- * ignoring case. A name that still matches several things is a question for
13
- * `pickReference`.
14
- */
15
-
16
- import {
17
- isBrowsedReading,
18
- isNamedReading,
19
- namespaceOfKey,
20
- parseRef,
21
- refKey,
22
- splitRecipeKey,
23
- type ParsedRef,
24
- type RefReading,
25
- } from "./parse.js";
26
- import { RefSource } from "./scopes.js";
27
-
28
- /**
29
- * The names that match a written one: those spelled exactly as written, or,
30
- * when there is none, those that differ only in case.
31
- *
32
- * matchNames("Workflow", ["workflow", "tooling"]); // -> ["workflow"]
33
- * matchNames("workflow", ["workflow", "Workflow"]); // -> ["workflow"]
34
- *
35
- * @param written - The name as written.
36
- * @param names - The names it could mean.
37
- */
38
- export function matchNames(written: string, names: Iterable<string>): string[] {
39
- const all = [...names];
40
- const exact = all.filter((name) => name === written);
41
- if (exact.length > 0) return exact;
42
- const folded = written.toLowerCase();
43
- return all.filter((name) => name.toLowerCase() === folded);
44
- }
45
-
46
- /** What settling needs to know about one repository's index. */
47
- export type SettleIndex = {
48
- /** Every namespace the index publishes. */
49
- namespaces: Iterable<string>;
50
- /** Every recipe it publishes, keyed `namespace/recipe`, with the folder it lives in. */
51
- recipes: Record<string, { path: string }>;
52
- };
53
-
54
- /**
55
- * What a reading names in one repository's index, spelled the way the index
56
- * spells it. A named reading that the index does not publish settles to
57
- * nothing; a browsed path settles through the recipe paths.
58
- *
59
- * settleInIndex({ namespace: "Workflow", recipe: "alpha" }, index);
60
- * // -> [{ namespace: "workflow", recipe: "alpha" }]
61
- *
62
- * @param reading - One reading of a written ref.
63
- * @param index - The index of the repository the reading points at.
64
- */
65
- export function settleInIndex(reading: RefReading, index: SettleIndex): ParsedRef[] {
66
- const carried = {
67
- ...(reading.location === undefined ? {} : { location: reading.location }),
68
- ...("range" in reading && reading.range !== undefined ? { range: reading.range } : {}),
69
- ...("repo" in reading && reading.repo !== undefined ? { repo: reading.repo } : {}),
70
- };
71
-
72
- if (isBrowsedReading(reading)) {
73
- return settleBrowsed(reading.browsed, index).map((settled) => ({ ...settled, ...carried }));
74
- }
75
- if (!isNamedReading(reading)) return [];
76
-
77
- if (reading.recipe === undefined) {
78
- return matchNames(reading.namespace, index.namespaces).map((namespace) => ({
79
- namespace,
80
- ...carried,
81
- }));
82
- }
83
-
84
- return matchNames(refKey(reading), Object.keys(index.recipes)).map((key) => {
85
- const { namespace, name } = splitRecipeKey(key);
86
- return { namespace, recipe: name, ...carried };
87
- });
88
- }
89
-
90
- /**
91
- * What a browser path names, through the folder each recipe's index entry
92
- * records. The path still carries the branch in front (which may itself hold
93
- * slashes), so every split is tried: the folder of a recipe, or anything inside
94
- * it, names that recipe; a folder holding recipes of exactly one namespace
95
- * names that namespace.
96
- *
97
- * settleBrowsed("main/recipes/workflow/alpha/SKILL.md", index);
98
- * // -> [{ namespace: "workflow", recipe: "alpha" }]
99
- *
100
- * @param browsed - The path after `tree/` or `blob/`, branch included.
101
- * @param index - The repository's index.
102
- */
103
- export function settleBrowsed(browsed: string, index: SettleIndex): ParsedRef[] {
104
- const segments = browsed.split("/").filter((segment) => segment.length > 0);
105
- const recipes = Object.entries(index.recipes).map(([key, entry]) => ({
106
- key,
107
- path: entry.path.replace(/^\.?\/+|\/+$/g, ""),
108
- }));
109
-
110
- const found = new Map<string, ParsedRef>();
111
-
112
- // A recipe's folder, or a file inside it. The deepest recipe wins, so a
113
- // recipe nested inside another's folder is named by its own path.
114
- for (let start = 1; start < segments.length; start++) {
115
- const folder = segments.slice(start).join("/");
116
- const holding = recipes
117
- .filter((recipe) => folder === recipe.path || folder.startsWith(`${recipe.path}/`))
118
- .sort((left, right) => right.path.length - left.path.length);
119
- const deepest = holding[0];
120
- if (deepest !== undefined) {
121
- const { namespace, name } = splitRecipeKey(deepest.key);
122
- found.set(deepest.key, { namespace, recipe: name });
123
- }
124
- }
125
- if (found.size > 0) return [...found.values()];
126
-
127
- // A folder above recipes: a namespace, when everything under it is one.
128
- for (let start = 1; start < segments.length; start++) {
129
- const folder = segments.slice(start).join("/");
130
- const under = recipes.filter((recipe) => recipe.path.startsWith(`${folder}/`));
131
- const namespaces = new Set(under.map((recipe) => namespaceOfKey(recipe.key)));
132
- if (namespaces.size === 1) {
133
- const namespace = [...namespaces][0]!;
134
- found.set(namespace, { namespace });
135
- }
136
- }
137
- return [...found.values()];
138
- }
139
-
140
- /** What a repository's index recorded about one dependency, as far as settling needs. */
141
- export type RecordedDependencies = Record<string, { repo?: string } | undefined>;
142
-
143
- /**
144
- * The readings of a manifest dependency that the declaring recipe's index
145
- * confirms. A release settles a dependency that reads more than one way (a
146
- * GitLab URL with nested groups, a browser URL) and records the repository it
147
- * settled on beside each key it resolved; a consumer reads that record and never
148
- * probes. A dependency that reads one way needs no record and comes back as it
149
- * is.
150
- *
151
- * A named reading is confirmed by a record under its own key (or, for a whole
152
- * namespace, under a key inside it) carrying its repository; a browsed reading
153
- * by any record carrying its repository.
154
- *
155
- * @param readings - Every reading of the dependency, from `parseRef`.
156
- * @param recorded - What the declaring version's index entry records, keyed `namespace/recipe`.
157
- * @returns The confirmed readings: one is the answer; none or several leave it unsettled.
158
- */
159
- export function confirmedReadings(
160
- readings: RefReading[],
161
- recorded: RecordedDependencies | undefined
162
- ): RefReading[] {
163
- if (readings.length === 1) return readings;
164
- const records = Object.entries(recorded ?? {});
165
-
166
- return readings.filter((reading) => {
167
- const identity = reading.location?.identity;
168
- if (identity === undefined) return false;
169
- return records.some(([key, record]) => {
170
- if (record?.repo !== identity) return false;
171
- if (isBrowsedReading(reading)) return true;
172
- if (!isNamedReading(reading)) return false;
173
- return reading.recipe === undefined
174
- ? key.startsWith(`${reading.namespace}/`)
175
- : key === refKey(reading);
176
- });
177
- });
178
- }
179
-
180
- /** What `settleDependency` may consult, beyond the dependency itself. */
181
- export type SettleDependencyOptions = {
182
- /** What the declaring version's index entry records about its dependencies. */
183
- recorded?: RecordedDependencies;
184
- /**
185
- * Whether a reading is already known to be right from what is on this
186
- * machine: its repository is trusted and its cached index publishes what it
187
- * names. Used only when the index recorded nothing, and it never fetches.
188
- */
189
- known?: (reading: RefReading) => boolean;
190
- };
191
-
192
- /**
193
- * The one reading a manifest dependency settles on, or undefined when it reads
194
- * more than one way and nothing at hand says which: the declaring recipe's
195
- * index recorded no answer, and what this machine already knows does not pick
196
- * exactly one.
197
- *
198
- * settleDependency("workflow/alpha");
199
- * // -> { namespace: "workflow", recipe: "alpha" }
200
- *
201
- * @param written - The dependency as the manifest wrote it.
202
- * @param options - The index's record, and what is known locally.
203
- */
204
- export function settleDependency(
205
- written: string,
206
- options: SettleDependencyOptions = {}
207
- ): RefReading | undefined {
208
- const readings = parseRef(written, RefSource.Manifest);
209
- if (readings.length === 1) return readings[0];
210
-
211
- const confirmed = confirmedReadings(readings, options.recorded);
212
- if (confirmed.length === 1) return confirmed[0];
213
-
214
- if (options.known !== undefined) {
215
- const known = readings.filter(options.known);
216
- if (known.length === 1) return known[0];
217
- }
218
- return undefined;
219
- }