@canonical/pragma-cli 0.32.0 → 0.34.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.
- package/README.md +72 -0
- package/dist/pragma +0 -0
- package/package.json +19 -15
- package/pragma.conf.ts +664 -0
- package/src/bin.ts +15 -6
- package/src/capabilities/block.parity.test.ts +250 -0
- package/src/capabilities/capabilities/capabilities.render.ts +3 -2
- package/src/capabilities/capabilities/capabilities.test.ts +10 -1
- package/src/capabilities/capabilities/capabilities.verb.ts +32 -14
- package/src/capabilities/capabilities/catalog.ts +20 -8
- package/src/capabilities/capabilities/hints.ts +21 -36
- package/src/capabilities/capabilities/types.ts +2 -0
- package/src/capabilities/colophon/collectColophon.ts +34 -19
- package/src/capabilities/colophon/colophon.render.test.ts +31 -4
- package/src/capabilities/colophon/colophon.render.ts +13 -4
- package/src/capabilities/colophon/colophon.test.ts +3 -3
- package/src/capabilities/colophon/colophon.verb.ts +11 -4
- package/src/capabilities/colophon/markdownTerminal.ts +33 -14
- package/src/capabilities/colophon/types.ts +16 -7
- package/src/capabilities/config/collectConfigShow.test.ts +57 -0
- package/src/capabilities/config/collectConfigShow.ts +41 -4
- package/src/capabilities/config/fields.ts +10 -108
- package/src/capabilities/config/set.test.ts +6 -6
- package/src/capabilities/config/set.verb.ts +12 -8
- package/src/capabilities/config/show.render.test.ts +137 -0
- package/src/capabilities/config/show.render.ts +57 -16
- package/src/capabilities/config/show.verb.ts +9 -8
- package/src/capabilities/config/types.ts +8 -0
- package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
- package/src/capabilities/create/byteEquality.test.ts +82 -101
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +53 -16
- package/src/capabilities/create/constants.ts +75 -0
- package/src/capabilities/create/create.mcp.test.ts +2 -2
- package/src/capabilities/create/create.test.ts +192 -16
- package/src/capabilities/create/create.verb.ts +208 -79
- package/src/capabilities/create/index.ts +2 -10
- package/src/capabilities/create/pickGenerator.ts +34 -28
- package/src/capabilities/create/templates.embedded.generated.ts +4 -29
- package/src/capabilities/create/types.ts +10 -6
- package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
- package/src/capabilities/distribution.test.ts +120 -0
- package/src/capabilities/distribution.ts +59 -0
- package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
- package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
- package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +15 -8
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
- package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
- package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
- package/src/capabilities/doctor/checks/deriveBand.test.ts +42 -0
- package/src/capabilities/doctor/checks/deriveBand.ts +28 -0
- package/src/capabilities/doctor/doctor.render.test.ts +170 -0
- package/src/capabilities/doctor/doctor.render.ts +117 -34
- package/src/capabilities/doctor/doctor.test.ts +68 -9
- package/src/capabilities/doctor/doctor.verb.ts +6 -2
- package/src/capabilities/doctor/runChecks.ts +33 -9
- package/src/capabilities/doctor/types.ts +16 -1
- package/src/capabilities/emptyRecovery.test.ts +32 -15
- package/src/capabilities/graph/inspect.verb.ts +3 -2
- package/src/capabilities/graph/query.verb.ts +3 -2
- package/src/capabilities/graph/runQuery.ts +8 -5
- package/src/capabilities/index.ts +22 -18
- package/src/capabilities/info/collectInfo.ts +10 -6
- package/src/capabilities/info/info.render.ts +3 -2
- package/src/capabilities/info/info.verb.ts +3 -2
- package/src/capabilities/lazy.test.ts +93 -2
- package/src/capabilities/ontology/index.ts +10 -3
- package/src/capabilities/ontology/ontology.test.ts +157 -5
- package/src/capabilities/ontology/verbs.ts +203 -104
- package/src/capabilities/prompt/prompt.render.test.ts +94 -0
- package/src/capabilities/prompt/prompt.render.ts +23 -6
- package/src/capabilities/prompt/prompt.test.ts +68 -1
- package/src/capabilities/prompt/verbs.ts +24 -11
- package/src/capabilities/reference.test.ts +32 -0
- package/src/capabilities/resources/provider.ts +58 -25
- package/src/capabilities/resources/resources.test.ts +176 -21
- package/src/capabilities/scopeTypeSync.test.ts +37 -0
- package/src/capabilities/setup/index.ts +7 -1
- package/src/capabilities/setup/operations/setupCompletions.ts +49 -16
- package/src/capabilities/setup/operations/setupGenerator.ts +166 -63
- package/src/capabilities/setup/operations/setupLsp.ts +56 -12
- package/src/capabilities/setup/operations/setupMcp.ts +216 -39
- package/src/capabilities/setup/operations/setupSkills.ts +10 -4
- package/src/capabilities/setup/setup.render.test.ts +109 -0
- package/src/capabilities/setup/setup.render.ts +61 -14
- package/src/capabilities/setup/setup.test.ts +352 -1
- package/src/capabilities/setup/setup.verb.ts +68 -9
- package/src/capabilities/setup/shell.ts +29 -3
- package/src/capabilities/setup/types.ts +59 -2
- package/src/capabilities/shared/bands.ts +16 -0
- package/src/capabilities/skill/discover.ts +11 -5
- package/src/capabilities/skill/render.ts +4 -5
- package/src/capabilities/skill/verbs.ts +7 -7
- package/src/capabilities/sources/collectStatus.ts +69 -49
- package/src/capabilities/sources/index.ts +1 -1
- package/src/capabilities/sources/installSkills.ts +2 -2
- package/src/capabilities/sources/runUpdate.ts +146 -98
- package/src/capabilities/sources/sources.test.ts +264 -84
- package/src/capabilities/sources/status.render.test.ts +94 -0
- package/src/capabilities/sources/status.render.ts +55 -33
- package/src/capabilities/sources/status.verb.ts +15 -8
- package/src/capabilities/sources/types.ts +18 -25
- package/src/capabilities/sources/update.render.ts +9 -2
- package/src/capabilities/sources/update.verb.ts +12 -23
- package/src/capabilities/surface.test.ts +65 -17
- package/src/capabilities/tier.parity.test.ts +144 -0
- package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
- package/src/constants.test.ts +9 -7
- package/src/constants.ts +49 -23
- package/src/identity.test.ts +333 -0
- package/src/index.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +18 -16
- package/src/kernel/completion/complete.test.ts +3 -5
- package/src/kernel/completion/emitScripts.test.ts +1 -1
- package/src/kernel/completion/emitScripts.ts +4 -2
- package/src/kernel/completion/entitySource.test.ts +185 -41
- package/src/kernel/completion/entitySource.ts +131 -113
- package/src/kernel/completion/model.test.ts +3 -3
- package/src/kernel/completion/model.ts +11 -1
- package/src/kernel/completion/parse.test.ts +2 -4
- package/src/kernel/completion/safety.test.ts +82 -36
- package/src/kernel/completion/shellDrive.test.ts +592 -0
- package/src/kernel/completion/templates/fish.ts +28 -6
- package/src/kernel/completion/types.ts +3 -3
- package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
- package/src/kernel/config/defaults.test.ts +85 -0
- package/src/kernel/config/defaults.ts +12 -23
- package/src/kernel/config/evaluateProjectConfig.ts +2 -3
- package/src/kernel/config/findProjectConfig.ts +4 -3
- package/src/kernel/config/firstRun.ts +12 -5
- package/src/kernel/config/globalConfig.ts +2 -2
- package/src/kernel/config/index.ts +4 -5
- package/src/kernel/config/paths.ts +17 -17
- package/src/kernel/config/readConfig.test.ts +242 -6
- package/src/kernel/config/readConfig.ts +8 -8
- package/src/kernel/config/schema.test.ts +50 -0
- package/src/kernel/config/schema.ts +97 -10
- package/src/kernel/config/types.ts +73 -19
- package/src/kernel/copy.test.ts +453 -0
- package/src/kernel/error/PragmaError.test.ts +5 -5
- package/src/kernel/error/constants.ts +3 -2
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +22 -45
- package/src/kernel/error/recovery.ts +22 -27
- package/src/kernel/error/renderError.ts +1 -1
- package/src/kernel/error/types.ts +5 -4
- package/src/kernel/packs/collect.test.ts +195 -11
- package/src/kernel/packs/collect.ts +213 -58
- package/src/kernel/packs/compile.test.ts +163 -7
- package/src/kernel/packs/compile.ts +15 -12
- package/src/kernel/packs/disclosure.test.ts +5 -2
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
- package/src/kernel/packs/graphqlSource.test.ts +6 -3
- package/src/kernel/packs/iri.ts +5 -7
- package/src/kernel/packs/renderPack.ts +5 -3
- package/src/kernel/packs/resolveEntity.ts +21 -17
- package/src/kernel/packs/runBodies.ts +15 -14
- package/src/kernel/packs/schema.ts +96 -10
- package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
- package/src/kernel/packs/sparql/runSelect.ts +73 -34
- package/src/kernel/packs/toolDescription.test.ts +10 -2
- package/src/kernel/packs/types.ts +39 -4
- package/src/kernel/packs/uniqueness.ts +4 -4
- package/src/kernel/project/cli/buildProgram.test.ts +60 -11
- package/src/kernel/project/cli/buildProgram.ts +11 -0
- package/src/kernel/project/cli/dispatch.test.ts +67 -0
- package/src/kernel/project/cli/dispatch.ts +34 -12
- package/src/kernel/project/cli/globalFlags.test.ts +12 -4
- package/src/kernel/project/cli/globalFlags.ts +14 -14
- package/src/kernel/project/cli/helpFormat.ts +1 -1
- package/src/kernel/project/cli/rootHelp.ts +109 -107
- package/src/kernel/project/mcp/buildServer.ts +11 -6
- package/src/kernel/project/mcp/instructions.ts +12 -7
- package/src/kernel/project/mcp/mcpError.ts +52 -0
- package/src/kernel/project/mcp/parity.test.ts +39 -1
- package/src/kernel/project/mcp/prompts/prompts.test.ts +128 -5
- package/src/kernel/project/mcp/prompts/provider.ts +51 -7
- package/src/kernel/project/mcp/prompts/source.ts +68 -67
- package/src/kernel/project/mcp/registerVerb.ts +25 -6
- package/src/kernel/project/mcp/serve.ts +11 -3
- package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
- package/src/kernel/render/disclosure.ts +4 -2
- package/src/kernel/render/formatters.ts +5 -4
- package/src/kernel/render/prefixes.test.ts +36 -0
- package/src/kernel/render/prefixes.ts +40 -13
- package/src/kernel/render/renderers.test.ts +35 -1
- package/src/kernel/render/renderers.ts +25 -7
- package/src/kernel/render/style.test.ts +31 -0
- package/src/kernel/render/style.ts +74 -0
- package/src/kernel/runtime/graphpack/build.ts +57 -3
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +149 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +173 -50
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -8
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -4
- package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +37 -11
- package/src/kernel/runtime/graphpack/graphpack.test.ts +274 -6
- package/src/kernel/runtime/graphpack/hash.ts +3 -2
- package/src/kernel/runtime/graphpack/index.ts +1 -1
- package/src/kernel/runtime/graphpack/manifest.ts +114 -12
- package/src/kernel/runtime/graphpack/read.ts +32 -22
- package/src/kernel/runtime/graphpack/schemas.ts +74 -0
- package/src/kernel/runtime/graphpack/stories.ts +84 -0
- package/src/kernel/runtime/graphpack/types.ts +72 -45
- package/src/kernel/runtime/loadSession.ts +4 -12
- package/src/kernel/runtime/paths.ts +64 -15
- package/src/kernel/runtime/probe.test.ts +8 -4
- package/src/kernel/runtime/readEntity.ts +6 -5
- package/src/kernel/runtime/refs/gitOps.ts +1 -19
- package/src/kernel/runtime/refs/parseRef.ts +7 -7
- package/src/kernel/runtime/refs/resolve.ts +141 -65
- package/src/kernel/runtime/resolveSources.test.ts +63 -24
- package/src/kernel/runtime/resolveSources.ts +36 -25
- package/src/kernel/runtime/storeReadiness.ts +62 -0
- package/src/kernel/runtime/storeless.test.ts +1 -1
- package/src/kernel/runtime/types.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +25 -20
- package/src/kernel/spec/emitReference.test.ts +196 -0
- package/src/kernel/spec/emitReference.ts +624 -0
- package/src/kernel/spec/emitSurface.ts +13 -7
- package/src/kernel/spec/index.ts +2 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/surfaceConformance.ts +8 -0
- package/src/kernel/spec/types.ts +29 -9
- package/src/kernel/spec/validate.ts +2 -1
- package/src/kernel/vocabulary.test.ts +164 -0
- package/src/kernel/vocabulary.ts +146 -0
- package/src/testing/PARITY_GAPS.ts +9 -3
- package/src/testing/behavioral/README.md +3 -3
- package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
- package/src/testing/behavioral/completion.test.ts +21 -8
- package/src/testing/behavioral/disclosure.test.ts +12 -0
- package/src/testing/behavioral/docExamples.test.ts +283 -0
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +3 -2
- package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
- package/src/testing/behavioral/harness.foundation.test.ts +14 -5
- package/src/testing/behavioral/journeys.cli.test.ts +116 -73
- package/src/testing/behavioral/journeys.defaultPack.test.ts +477 -0
- package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
- package/src/testing/behavioral/journeys.interactiveTty.e2e.test.ts +66 -0
- package/src/testing/behavioral/journeys.livePack.e2e.test.ts +129 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +179 -0
- package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
- package/src/testing/behavioral/liveReadSurface.ts +4 -3
- package/src/testing/behavioral/mcpSurface.test.ts +10 -5
- package/src/testing/behavioral/staleCommands.test.ts +86 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +9 -9
- package/src/testing/eval/cases/assembly.ts +16 -9
- package/src/testing/eval/cases/effectful.ts +31 -16
- package/src/testing/eval/cases/readNouns.ts +23 -18
- package/src/testing/eval/cases/stable.ts +5 -1
- package/src/testing/eval/eval.test.ts +7 -1
- package/src/testing/fixtures/fixtureCapability.ts +201 -1
- package/src/testing/fixtures/graph/canonical.ts +6 -3
- package/src/testing/fixtures/graph/defaultPack.ts +179 -0
- package/src/testing/fixtures/graph/owlExport.ts +97 -0
- package/src/testing/helpers/extractSnippets.ts +29 -0
- package/src/testing/helpers/fixtureGraph.ts +33 -16
- package/src/testing/helpers/golden.ts +1 -1
- package/src/testing/helpers/packRuntime.ts +2 -3
- package/src/testing/helpers/projectCli.ts +2 -1
- package/src/testing/helpers/projectMcp.ts +6 -0
- package/src/testing/perf/budgets.test.ts +4 -0
- package/src/testing/perf/budgets.ts +22 -7
- package/src/testing/perf/globalSetup.ts +121 -6
- package/src/types.ts +2 -2
- package/src/capabilities/block/blockList.verb.ts +0 -150
- package/src/capabilities/block/index.ts +0 -25
- package/src/capabilities/block/pack.ts +0 -155
- package/src/capabilities/block/parity.test.ts +0 -180
- package/src/capabilities/block/tierChain.ts +0 -78
- package/src/capabilities/colophon/pragmaColophon.ts +0 -66
- package/src/capabilities/config/field.test.ts +0 -188
- package/src/capabilities/modifier/index.ts +0 -15
- package/src/capabilities/modifier/pack.ts +0 -64
- package/src/capabilities/standard/index.ts +0 -18
- package/src/capabilities/standard/pack.ts +0 -128
- package/src/capabilities/tier/index.ts +0 -19
- package/src/capabilities/tier/lookup.render.ts +0 -34
- package/src/capabilities/tier/lookup.test.ts +0 -68
- package/src/capabilities/tier/lookup.verb.ts +0 -44
- package/src/capabilities/tier/pack.ts +0 -28
- package/src/capabilities/tier/runLookup.ts +0 -59
- package/src/capabilities/token/addConfig.render.ts +0 -27
- package/src/capabilities/token/addConfig.test.ts +0 -62
- package/src/capabilities/token/addConfig.verb.ts +0 -40
- package/src/capabilities/token/index.ts +0 -19
- package/src/capabilities/token/pack.ts +0 -58
- package/src/capabilities/token/runAddConfig.ts +0 -95
- package/src/kernel/completion/bashDrive.test.ts +0 -73
- package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
- package/src/kernel/runtime/lock.ts +0 -90
|
@@ -1,73 +1,107 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Doctor check: report
|
|
2
|
+
* Doctor check: report which pack answers this project's reads.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
4
|
+
* It switches on the ONE boot decision ({@link resolveSources}) rather than
|
|
5
|
+
* re-deriving it from config, so doctor can never call an install healthy that
|
|
6
|
+
* every read would fail. That is the whole point of the check: a project that
|
|
7
|
+
* configured its own packs and never built them reports `fail` with the update
|
|
8
|
+
* remedy — not a pass listing the distribution's packs it is not reading.
|
|
9
|
+
* Storeless throughout: the pointer, the manifest, and the entity index are
|
|
10
|
+
* read off disk, never through a store session.
|
|
11
|
+
*
|
|
12
|
+
* It also names every read story that pack carries but cannot use. Those are
|
|
13
|
+
* DROPPED at dispatch rather than thrown (a third-party story must never break
|
|
14
|
+
* a command), so doctor is where the user finds out — and it goes through the
|
|
15
|
+
* same `validateStories` the dispatch path uses, so the two can never disagree.
|
|
9
16
|
*/
|
|
10
17
|
|
|
18
|
+
import { BIN_NAME } from "../../../constants.js";
|
|
11
19
|
import {
|
|
12
20
|
entityTotal,
|
|
13
21
|
readPackIndex,
|
|
14
22
|
} from "../../../kernel/completion/entitySource.js";
|
|
15
|
-
import
|
|
16
|
-
import {
|
|
23
|
+
import { validateStories } from "../../../kernel/packs/collect.js";
|
|
24
|
+
import { embeddedManifest } from "../../../kernel/runtime/graphpack/embedded.js";
|
|
25
|
+
import { readManifest } from "../../../kernel/runtime/graphpack/manifest.js";
|
|
26
|
+
import { activeStories } from "../../../kernel/runtime/graphpack/stories.js";
|
|
27
|
+
import type { SourcesDecision } from "../../../kernel/runtime/resolveSources.js";
|
|
28
|
+
import { resolveSources } from "../../../kernel/runtime/resolveSources.js";
|
|
17
29
|
import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
|
|
18
30
|
import type { CheckItem, CheckResult } from "../types.js";
|
|
19
31
|
|
|
20
|
-
|
|
21
|
-
|
|
32
|
+
/**
|
|
33
|
+
* The stories the answering pack carries but cannot use, as check items.
|
|
34
|
+
*
|
|
35
|
+
* The registry is reached through a RUNTIME dynamic import (as
|
|
36
|
+
* `collectColophon` does) so the static `capabilities/index → doctorModule →
|
|
37
|
+
* checkPackageRefs → capabilities/index` cycle can never form.
|
|
38
|
+
*
|
|
39
|
+
* @param decision - The boot decision the check already resolved.
|
|
40
|
+
* @returns One failing item per ignored story, in declaration order.
|
|
41
|
+
* @note Impure — reads the answering pack's `stories.json`.
|
|
42
|
+
*/
|
|
43
|
+
async function ignoredStoryItems(
|
|
44
|
+
decision: SourcesDecision,
|
|
45
|
+
): Promise<CheckItem[]> {
|
|
46
|
+
const { capabilities } = await import("../../index.js");
|
|
47
|
+
const { problems } = validateStories(activeStories(decision), capabilities);
|
|
48
|
+
return problems.map((problem) => ({
|
|
49
|
+
label: problem.source,
|
|
50
|
+
detail: problem.message,
|
|
51
|
+
status: "fail" as const,
|
|
52
|
+
}));
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** ` · N story/ies ignored` for a headline, or nothing when all are usable. */
|
|
56
|
+
function ignoredSuffix(items: readonly CheckItem[]): string {
|
|
57
|
+
if (items.length === 0) return "";
|
|
58
|
+
return ` · ${items.length} ${items.length === 1 ? "story" : "stories"} ignored`;
|
|
59
|
+
}
|
|
22
60
|
|
|
23
61
|
/**
|
|
24
|
-
* Report
|
|
62
|
+
* Report which pack answers reads, its provenance, and its entity total.
|
|
25
63
|
*
|
|
26
64
|
* @param rt - The per-invocation runtime.
|
|
27
|
-
* @returns A CheckResult
|
|
28
|
-
* @note Impure — reads config, the
|
|
65
|
+
* @returns A CheckResult naming the answering pack, or the failure to build one.
|
|
66
|
+
* @note Impure — reads config, the active-pack pointer, and the pack index.
|
|
29
67
|
*/
|
|
30
68
|
export async function checkPackageRefs(
|
|
31
69
|
rt: PragmaRuntime,
|
|
32
70
|
): Promise<CheckResult> {
|
|
33
|
-
const
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
const
|
|
71
|
+
const decision = resolveSources(await rt.loadConfig(), rt.cwd);
|
|
72
|
+
// The display says "packs" (config declarations); the file/function keep
|
|
73
|
+
// "Package" (npm/git resolution vocabulary).
|
|
74
|
+
const name = "pack refs";
|
|
37
75
|
|
|
38
|
-
if (
|
|
76
|
+
if (decision.kind === "unavailable") {
|
|
39
77
|
return {
|
|
40
|
-
name
|
|
41
|
-
status: "
|
|
42
|
-
detail:
|
|
78
|
+
name,
|
|
79
|
+
status: "fail",
|
|
80
|
+
detail: decision.reason,
|
|
81
|
+
remedy: `${BIN_NAME} sources update`,
|
|
43
82
|
};
|
|
44
83
|
}
|
|
45
84
|
|
|
46
|
-
const
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
items.
|
|
59
|
-
|
|
60
|
-
}
|
|
85
|
+
const index = readPackIndex(decision);
|
|
86
|
+
const entities = (index ? entityTotal(index) : 0).toLocaleString();
|
|
87
|
+
// The pack DOES answer reads, so the check itself passes; the ignored stories
|
|
88
|
+
// are failing sub-items under it. (A `warn` status would ripple through the
|
|
89
|
+
// renderer and the DoctorData counts to buy an icon.)
|
|
90
|
+
const items = await ignoredStoryItems(decision);
|
|
91
|
+
const suffix = ignoredSuffix(items);
|
|
92
|
+
if (decision.kind === "embedded") {
|
|
93
|
+
return {
|
|
94
|
+
name,
|
|
95
|
+
status: "pass",
|
|
96
|
+
detail: `embedded snapshot @ ${embeddedManifest().sourceRef} — ${entities} entities${suffix} · \`${BIN_NAME} sources update\` to build from the configured packs`,
|
|
97
|
+
...(items.length > 0 ? { items } : {}),
|
|
98
|
+
};
|
|
61
99
|
}
|
|
62
|
-
|
|
63
|
-
const ok = lock !== undefined && unresolved === 0;
|
|
100
|
+
const manifest = readManifest(decision.dir);
|
|
64
101
|
return {
|
|
65
|
-
name
|
|
66
|
-
status:
|
|
67
|
-
detail:
|
|
68
|
-
|
|
69
|
-
: `${unresolved} of ${entries.length} package(s) not resolved`,
|
|
70
|
-
items,
|
|
71
|
-
...(ok ? {} : { remedy: "pragma sources update" }),
|
|
102
|
+
name,
|
|
103
|
+
status: "pass",
|
|
104
|
+
detail: `${manifest?.sourceRef ?? decision.contentHash.slice(0, 12)} — ${entities} entities, built ${manifest?.createdAt ?? "?"}${suffix}`,
|
|
105
|
+
...(items.length > 0 ? { items } : {}),
|
|
72
106
|
};
|
|
73
107
|
}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
|
+
import { BIN_NAME } from "../../../constants.js";
|
|
1
2
|
import type { PragmaRuntime } from "../../../kernel/runtime/types.js";
|
|
2
3
|
import { detectInstallSource } from "../../shared/packageManager.js";
|
|
3
4
|
import type { CheckResult } from "../types.js";
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
|
-
* Report the
|
|
7
|
+
* Report the CLI version and how it was installed. Always passes — purely
|
|
7
8
|
* informational.
|
|
8
9
|
*
|
|
9
10
|
* @param rt - The per-invocation runtime (for `version`).
|
|
@@ -15,7 +16,7 @@ export async function checkPragmaVersion(
|
|
|
15
16
|
): Promise<CheckResult> {
|
|
16
17
|
const install = detectInstallSource();
|
|
17
18
|
return {
|
|
18
|
-
name:
|
|
19
|
+
name: `${BIN_NAME} version`,
|
|
19
20
|
status: "pass",
|
|
20
21
|
detail: `v${rt.version} (installed via ${install.label})`,
|
|
21
22
|
};
|
|
@@ -1,19 +1,28 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* `checkShellCompletions` — the doctor check that verifies completions are
|
|
3
|
-
* installed AND functional.
|
|
3
|
+
* installed AND up to date AND functional.
|
|
4
4
|
*
|
|
5
5
|
* Runs against an isolated HOME and a controlled `$SHELL` so the three gates —
|
|
6
|
-
* the resolver effect test, the
|
|
7
|
-
*
|
|
6
|
+
* the resolver effect test, the install probe, and the zsh fpath activation
|
|
7
|
+
* gate — are deterministic. The effect test (gate 1) drives the real
|
|
8
8
|
* `runComplete` resolver; the install/fpath gates write real files under the
|
|
9
9
|
* temp HOME rather than mocking the fs.
|
|
10
|
+
*
|
|
11
|
+
* Gate 2 compares BYTES, not existence, so the "installed" cases here write the
|
|
12
|
+
* real `emitScripts(capabilities)[shell]` body. A file that merely exists is
|
|
13
|
+
* now the failing case, and it has its own test — as does the boundary on the
|
|
14
|
+
* other side, where a per-project `completion` config must NOT be able to
|
|
15
|
+
* condemn the one global script.
|
|
10
16
|
*/
|
|
11
17
|
|
|
12
18
|
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
13
19
|
import { tmpdir } from "node:os";
|
|
14
20
|
import { dirname, join } from "node:path";
|
|
15
21
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
16
|
-
import {
|
|
22
|
+
import { emitScripts } from "../../../kernel/completion/emitScripts.js";
|
|
23
|
+
import { capabilities } from "../../index.js";
|
|
24
|
+
import { detectCompletions } from "../../setup/operations/setupCompletions.js";
|
|
25
|
+
import { completionScriptPath, type ShellId } from "../../setup/shell.js";
|
|
17
26
|
import { checkShellCompletions } from "./checkShellCompletions.js";
|
|
18
27
|
|
|
19
28
|
const roots: string[] = [];
|
|
@@ -38,11 +47,16 @@ afterEach(() => {
|
|
|
38
47
|
roots.length = 0;
|
|
39
48
|
});
|
|
40
49
|
|
|
41
|
-
/** Write
|
|
42
|
-
function
|
|
50
|
+
/** Write `body` to the shell's real install path. */
|
|
51
|
+
function writeScript(shell: ShellId, body: string): void {
|
|
43
52
|
const path = completionScriptPath(shell);
|
|
44
53
|
mkdirSync(dirname(path), { recursive: true });
|
|
45
|
-
writeFileSync(path,
|
|
54
|
+
writeFileSync(path, body);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Install the script `setup completions` would write for this shell. */
|
|
58
|
+
function installScript(shell: ShellId): void {
|
|
59
|
+
writeScript(shell, emitScripts(capabilities)[shell]);
|
|
46
60
|
}
|
|
47
61
|
|
|
48
62
|
/** Write a `.zshrc` that puts ~/.zfunc on fpath. */
|
|
@@ -73,20 +87,56 @@ describe("checkShellCompletions — install probe (gate 2)", () => {
|
|
|
73
87
|
expect(result.remedy).toBe("pragma setup completions");
|
|
74
88
|
});
|
|
75
89
|
|
|
76
|
-
it("passes for bash once the script
|
|
90
|
+
it("passes for bash once the up-to-date script is at its real path", async () => {
|
|
77
91
|
process.env.SHELL = "/usr/bin/bash";
|
|
78
92
|
installScript("bash");
|
|
79
93
|
const result = await checkShellCompletions(tmp());
|
|
80
94
|
expect(result.status).toBe("pass");
|
|
81
|
-
expect(result.detail).toMatch(/bash
|
|
95
|
+
expect(result.detail).toMatch(/bash up to date and resolving/);
|
|
82
96
|
});
|
|
83
97
|
|
|
84
|
-
it("passes for fish once the script
|
|
98
|
+
it("passes for fish once the up-to-date script is at its real path", async () => {
|
|
85
99
|
process.env.SHELL = "/usr/bin/fish";
|
|
86
100
|
installScript("fish");
|
|
87
101
|
const result = await checkShellCompletions(tmp());
|
|
88
102
|
expect(result.status).toBe("pass");
|
|
89
103
|
});
|
|
104
|
+
|
|
105
|
+
it("fails when the installed script is out of date", async () => {
|
|
106
|
+
// Reproduced against the compiled binary before this was fixed: overwrite
|
|
107
|
+
// the installed script with junk and `doctor` still reported
|
|
108
|
+
// "✓ Shell completions: bash installed and resolving (19 nouns)", while
|
|
109
|
+
// `setup completions` in the same breath said "Updating bash completions".
|
|
110
|
+
// Every user upgrading across a grammar change lands here: the old script
|
|
111
|
+
// offers the old nouns and misses the new ones, and doctor is the one
|
|
112
|
+
// command whose job is to say so.
|
|
113
|
+
process.env.SHELL = "/usr/bin/bash";
|
|
114
|
+
writeScript("bash", "# STALE JUNK\n");
|
|
115
|
+
const result = await checkShellCompletions(tmp());
|
|
116
|
+
expect(result.status).toBe("fail");
|
|
117
|
+
expect(result.detail).toMatch(/out of date/);
|
|
118
|
+
expect(result.remedy).toBe("pragma setup completions");
|
|
119
|
+
});
|
|
120
|
+
|
|
121
|
+
it("does NOT condemn the global script for one project's `completion` config", async () => {
|
|
122
|
+
// The script is ONE file per user; `completion` config is layered per
|
|
123
|
+
// project. Comparing the global file against only THIS directory's body
|
|
124
|
+
// made `doctor`'s verdict flip with `cd`: reproduced against the real
|
|
125
|
+
// check, a project declaring `minChars: 5` passed in its own directory and
|
|
126
|
+
// failed in every other, and running the offered remedy there moved the
|
|
127
|
+
// failure back rather than closing it. The first assertion is the
|
|
128
|
+
// discriminator — this project really does ask for a different body — so
|
|
129
|
+
// deleting the config write below turns the test red.
|
|
130
|
+
process.env.SHELL = "/usr/bin/bash";
|
|
131
|
+
installScript("bash");
|
|
132
|
+
const cwd = tmp();
|
|
133
|
+
writeFileSync(
|
|
134
|
+
join(cwd, "pragma.config.ts"),
|
|
135
|
+
"export default { completion: { minChars: 5 } };\n",
|
|
136
|
+
);
|
|
137
|
+
expect((await detectCompletions(cwd)).state).toBe("stale");
|
|
138
|
+
expect((await checkShellCompletions(cwd)).status).toBe("pass");
|
|
139
|
+
});
|
|
90
140
|
});
|
|
91
141
|
|
|
92
142
|
describe("checkShellCompletions — zsh fpath activation (gate 3)", () => {
|
|
@@ -106,6 +156,6 @@ describe("checkShellCompletions — zsh fpath activation (gate 3)", () => {
|
|
|
106
156
|
wireZfunc();
|
|
107
157
|
const result = await checkShellCompletions(tmp());
|
|
108
158
|
expect(result.status).toBe("pass");
|
|
109
|
-
expect(result.detail).toMatch(/zsh
|
|
159
|
+
expect(result.detail).toMatch(/zsh up to date and resolving/);
|
|
110
160
|
});
|
|
111
161
|
});
|
|
@@ -1,20 +1,48 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import { readFileSync } from "node:fs";
|
|
2
2
|
import { homedir } from "node:os";
|
|
3
3
|
import { join } from "node:path";
|
|
4
|
+
import { BIN_NAME } from "../../../constants.js";
|
|
4
5
|
import {
|
|
6
|
+
emitScripts,
|
|
5
7
|
indexCompletionEnv,
|
|
6
8
|
runComplete,
|
|
7
9
|
} from "../../../kernel/completion/index.js";
|
|
8
10
|
import { capabilities } from "../../index.js";
|
|
9
|
-
import {
|
|
10
|
-
|
|
11
|
-
completionScriptPath,
|
|
12
|
-
detectShell,
|
|
13
|
-
} from "../../setup/shell.js";
|
|
11
|
+
import { detectCompletions } from "../../setup/operations/setupCompletions.js";
|
|
12
|
+
import { activationHint, type ShellId } from "../../setup/shell.js";
|
|
14
13
|
import type { CheckResult } from "../types.js";
|
|
15
14
|
|
|
16
15
|
const NAME = "Shell completions";
|
|
17
16
|
|
|
17
|
+
/** The one remedy for an absent or out-of-date script. */
|
|
18
|
+
const INSTALL_REMEDY = `${BIN_NAME} setup completions`;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Whether the installed script is the config-free emit — the body
|
|
22
|
+
* `setup completions` writes when no `completion` config tunes it.
|
|
23
|
+
*
|
|
24
|
+
* The completion script is ONE file per user, but the `completion` config it is
|
|
25
|
+
* rendered from is layered per project. So `detectCompletions(cwd)`'s `stale`
|
|
26
|
+
* means only "not what THIS directory would write", and on its own it would let
|
|
27
|
+
* any project that tunes `minChars` call a globally-correct script out of date
|
|
28
|
+
* from every other directory — a `doctor` verdict that flips with `cd`, and a
|
|
29
|
+
* remedy that just moves the failure to the project you came from. A script
|
|
30
|
+
* that matches neither this project's body nor the untuned one is the one that
|
|
31
|
+
* is genuinely stale.
|
|
32
|
+
*
|
|
33
|
+
* @param path - The install path.
|
|
34
|
+
* @param shell - The detected shell.
|
|
35
|
+
* @returns Whether the file there is byte-identical to the untuned emit.
|
|
36
|
+
* @note Impure — reads the install path. An unreadable file counts as `false`.
|
|
37
|
+
*/
|
|
38
|
+
function isUntunedScript(path: string, shell: ShellId): boolean {
|
|
39
|
+
try {
|
|
40
|
+
return readFileSync(path, "utf-8") === emitScripts(capabilities)[shell];
|
|
41
|
+
} catch {
|
|
42
|
+
return false;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
18
46
|
/**
|
|
19
47
|
* Whether the user's `.zshrc` puts `~/.zfunc` on `$fpath` — the activation step
|
|
20
48
|
* `setup completions` cannot perform for the user. zsh loads nothing from
|
|
@@ -53,23 +81,28 @@ async function completeProbe(cwd: string): Promise<number> {
|
|
|
53
81
|
}
|
|
54
82
|
|
|
55
83
|
/**
|
|
56
|
-
* Check that shell completions are installed AND functional.
|
|
84
|
+
* Check that shell completions are installed AND up to date AND functional.
|
|
57
85
|
*
|
|
58
86
|
* Three gates, in order:
|
|
59
87
|
* 1. The resolver answers (`completeProbe` drives `runComplete` end to end) —
|
|
60
88
|
* the effect every installed script depends on. A failure here is a real
|
|
61
89
|
* regression, so it fails even when nothing is installed.
|
|
62
|
-
* 2. The script
|
|
63
|
-
*
|
|
64
|
-
*
|
|
90
|
+
* 2. The installed script is present AND is a script `setup completions` would
|
|
91
|
+
* write. Bytes, not existence: a file that merely exists proves nothing — a
|
|
92
|
+
* user upgrading across a grammar change keeps the old script, loses the
|
|
93
|
+
* moved nouns from TAB, and used to be told everything was fine. The path
|
|
94
|
+
* and this project's body come from `detectCompletions`, the ONE owner of
|
|
95
|
+
* that decision; {@link isUntunedScript} is what keeps a per-project
|
|
96
|
+
* `completion` config from condemning a global file (see its docblock).
|
|
65
97
|
* 3. For zsh, `~/.zfunc` is on `$fpath` — the activation step setup can only
|
|
66
98
|
* hint. Installed-but-unwired reports a distinct remedy.
|
|
67
99
|
*
|
|
68
|
-
* @param cwd - The project directory (
|
|
69
|
-
*
|
|
100
|
+
* @param cwd - The project directory (the resolver's entity seam, and the
|
|
101
|
+
* `completion` config gate 2 renders this project's body from).
|
|
102
|
+
* @returns A CheckResult: pass (up to date + wired + answering), fail (with the
|
|
70
103
|
* attributable remedy), or skip (shell undetected).
|
|
71
|
-
* @note Impure — reads `$SHELL`, the install path, `.zshrc`,
|
|
72
|
-
* storeless resolver.
|
|
104
|
+
* @note Impure — reads `$SHELL`, the install path, the config layers, `.zshrc`,
|
|
105
|
+
* and drives the storeless resolver.
|
|
73
106
|
*/
|
|
74
107
|
export async function checkShellCompletions(cwd: string): Promise<CheckResult> {
|
|
75
108
|
// 1. Effect test: the resolver the scripts delegate to must actually answer.
|
|
@@ -82,34 +115,41 @@ export async function checkShellCompletions(cwd: string): Promise<CheckResult> {
|
|
|
82
115
|
name: NAME,
|
|
83
116
|
status: "fail",
|
|
84
117
|
detail: `completion resolver failed: ${reason}`,
|
|
85
|
-
remedy:
|
|
118
|
+
remedy: `Report this as a bug — \`${BIN_NAME} __complete\` should never throw.`,
|
|
86
119
|
};
|
|
87
120
|
}
|
|
88
121
|
if (candidates === 0) {
|
|
89
122
|
return {
|
|
90
123
|
name: NAME,
|
|
91
124
|
status: "fail",
|
|
92
|
-
detail:
|
|
125
|
+
detail: `completion resolver returned no candidates for \`${BIN_NAME} <TAB>\``,
|
|
93
126
|
remedy: "Report this as a bug — the noun context is always non-empty.",
|
|
94
127
|
};
|
|
95
128
|
}
|
|
96
129
|
|
|
97
|
-
// 2. The script
|
|
98
|
-
const shell =
|
|
99
|
-
if (
|
|
130
|
+
// 2. The installed script is a script `setup completions` would write.
|
|
131
|
+
const { shell, path, state } = await detectCompletions(cwd);
|
|
132
|
+
if (shell === null || path === null) {
|
|
100
133
|
return {
|
|
101
134
|
name: NAME,
|
|
102
135
|
status: "skip",
|
|
103
136
|
detail: "resolver OK; shell not detected ($SHELL unset)",
|
|
104
137
|
};
|
|
105
138
|
}
|
|
106
|
-
|
|
107
|
-
if (!existsSync(path)) {
|
|
139
|
+
if (state === "absent") {
|
|
108
140
|
return {
|
|
109
141
|
name: NAME,
|
|
110
142
|
status: "fail",
|
|
111
143
|
detail: `resolver OK; ${shell} script not installed`,
|
|
112
|
-
remedy:
|
|
144
|
+
remedy: INSTALL_REMEDY,
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
if (state === "stale" && !isUntunedScript(path, shell)) {
|
|
148
|
+
return {
|
|
149
|
+
name: NAME,
|
|
150
|
+
status: "fail",
|
|
151
|
+
detail: `resolver OK; ${shell} script at ${path} is out of date`,
|
|
152
|
+
remedy: INSTALL_REMEDY,
|
|
113
153
|
};
|
|
114
154
|
}
|
|
115
155
|
|
|
@@ -126,6 +166,6 @@ export async function checkShellCompletions(cwd: string): Promise<CheckResult> {
|
|
|
126
166
|
return {
|
|
127
167
|
name: NAME,
|
|
128
168
|
status: "pass",
|
|
129
|
-
detail: `${shell}
|
|
169
|
+
detail: `${shell} up to date and resolving (${candidates} nouns)`,
|
|
130
170
|
};
|
|
131
171
|
}
|
|
@@ -2,6 +2,7 @@ import { existsSync } from "node:fs";
|
|
|
2
2
|
import type { DetectedHarness } from "@canonical/harnesses";
|
|
3
3
|
import { detectHarnesses } from "@canonical/harnesses";
|
|
4
4
|
import { runTask } from "@canonical/task/node";
|
|
5
|
+
import { BIN_NAME } from "../../../constants.js";
|
|
5
6
|
import type { CheckResult } from "../types.js";
|
|
6
7
|
|
|
7
8
|
/**
|
|
@@ -21,7 +22,7 @@ export async function checkSkillsSymlinked(cwd: string): Promise<CheckResult> {
|
|
|
21
22
|
name: "Skills symlinked",
|
|
22
23
|
status: "fail",
|
|
23
24
|
detail: "harness detection failed",
|
|
24
|
-
remedy:
|
|
25
|
+
remedy: `${BIN_NAME} setup skills`,
|
|
25
26
|
};
|
|
26
27
|
}
|
|
27
28
|
|
|
@@ -55,6 +56,6 @@ export async function checkSkillsSymlinked(cwd: string): Promise<CheckResult> {
|
|
|
55
56
|
name: "Skills symlinked",
|
|
56
57
|
status: "fail",
|
|
57
58
|
detail: `missing for ${missing.join(", ")}`,
|
|
58
|
-
remedy:
|
|
59
|
+
remedy: `${BIN_NAME} setup skills`,
|
|
59
60
|
};
|
|
60
61
|
}
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `deriveBand` — band a doctor check by the harnesses it actually found.
|
|
3
|
+
*
|
|
4
|
+
* Fixtures wrap REAL harness definitions (via `findHarnessById`, no casts), so
|
|
5
|
+
* the scope→band mapping is exercised against the true `scope` fields: the
|
|
6
|
+
* regression this guards is a global-scope harness (Windsurf) being mislabeled
|
|
7
|
+
* PROJECT by a static per-name guess.
|
|
8
|
+
*/
|
|
9
|
+
|
|
10
|
+
import type { DetectedHarness } from "@canonical/harnesses";
|
|
11
|
+
import { findHarnessById } from "@canonical/harnesses";
|
|
12
|
+
import { describe, expect, it } from "vitest";
|
|
13
|
+
import { deriveBand } from "./deriveBand.js";
|
|
14
|
+
|
|
15
|
+
/** A minimal {@link DetectedHarness} over a real definition (no `as` casts). */
|
|
16
|
+
function detect(id: string): DetectedHarness {
|
|
17
|
+
const harness = findHarnessById(id);
|
|
18
|
+
if (!harness) throw new Error(`unknown harness fixture: ${id}`);
|
|
19
|
+
return { harness, confidence: "high", configExists: true, configPath: "x" };
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
describe("deriveBand", () => {
|
|
23
|
+
it("bands a project-scope harness (Cursor) as project", () => {
|
|
24
|
+
expect(deriveBand([detect("cursor")])).toBe("project");
|
|
25
|
+
});
|
|
26
|
+
|
|
27
|
+
it("bands a global-scope harness (Windsurf) as global — the fixed mislabel", () => {
|
|
28
|
+
expect(deriveBand([detect("windsurf")])).toBe("global");
|
|
29
|
+
});
|
|
30
|
+
|
|
31
|
+
it("bands a dual-scope harness (Claude Code) by its default band (project)", () => {
|
|
32
|
+
expect(deriveBand([detect("claude-code")])).toBe("project");
|
|
33
|
+
});
|
|
34
|
+
|
|
35
|
+
it("leaves a set spanning BOTH bands unbanded (general, not mislabeled)", () => {
|
|
36
|
+
expect(deriveBand([detect("cursor"), detect("windsurf")])).toBeUndefined();
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
it("leaves an empty set unbanded", () => {
|
|
40
|
+
expect(deriveBand([])).toBeUndefined();
|
|
41
|
+
});
|
|
42
|
+
});
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Derive a doctor check's config band from the harnesses it actually found,
|
|
3
|
+
* rather than a static per-check-name guess. The aggregate MCP checks read each
|
|
4
|
+
* harness's DEFAULT-band file (`defaultBandOf` — the home config for a
|
|
5
|
+
* global-only harness like Windsurf, the project file otherwise), so the band a
|
|
6
|
+
* finding belongs to is exactly that default band.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import type { DetectedHarness } from "@canonical/harnesses";
|
|
10
|
+
import { defaultBandOf } from "@canonical/harnesses";
|
|
11
|
+
import type { ScopeBand } from "../types.js";
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* The single config band a set of detected harnesses shares, or `undefined` when
|
|
15
|
+
* the set is empty or genuinely spans BOTH bands. A two-band finding stays
|
|
16
|
+
* unbanded (rendered in the general section) instead of being mislabeled as one
|
|
17
|
+
* band — faithful to the two-band model: band by what was found, never a guess.
|
|
18
|
+
*
|
|
19
|
+
* @param harnesses - The detected harnesses the check reports on.
|
|
20
|
+
* @returns The shared {@link ScopeBand}, or `undefined` when none/mixed.
|
|
21
|
+
*/
|
|
22
|
+
export function deriveBand(
|
|
23
|
+
harnesses: readonly DetectedHarness[],
|
|
24
|
+
): ScopeBand | undefined {
|
|
25
|
+
const bands = new Set<ScopeBand>();
|
|
26
|
+
for (const detected of harnesses) bands.add(defaultBandOf(detected.harness));
|
|
27
|
+
return bands.size === 1 ? bands.values().next().value : undefined;
|
|
28
|
+
}
|