@canonical/pragma-cli 0.32.0 → 0.33.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 +14 -13
- package/src/bin.ts +2 -2
- package/src/capabilities/block/blockList.test.ts +79 -0
- package/src/capabilities/block/blockList.verb.ts +18 -5
- package/src/capabilities/block/pack.ts +12 -1
- package/src/capabilities/block/parity.test.ts +9 -2
- package/src/capabilities/capabilities/capabilities.test.ts +10 -1
- package/src/capabilities/capabilities/capabilities.verb.ts +1 -1
- package/src/capabilities/capabilities/catalog.ts +7 -3
- package/src/capabilities/capabilities/hints.ts +8 -17
- package/src/capabilities/capabilities/types.ts +2 -0
- 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 +4 -1
- package/src/capabilities/colophon/markdownTerminal.ts +33 -14
- package/src/capabilities/colophon/pragmaColophon.ts +4 -4
- package/src/capabilities/colophon/types.ts +1 -1
- package/src/capabilities/config/fields.ts +21 -92
- package/src/capabilities/config/set.test.ts +6 -6
- package/src/capabilities/config/set.verb.ts +5 -5
- package/src/capabilities/config/show.render.test.ts +65 -0
- package/src/capabilities/config/show.render.ts +56 -10
- package/src/capabilities/config/show.verb.ts +5 -5
- package/src/capabilities/create/byteEquality.subprocess.test.ts +1 -1
- package/src/capabilities/create/byteEquality.test.ts +7 -4
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +1 -1
- package/src/capabilities/create/create.test.ts +32 -3
- package/src/capabilities/create/create.verb.ts +69 -11
- package/src/capabilities/doctor/checks/checkConfigFile.ts +1 -1
- package/src/capabilities/doctor/checks/checkMcpCommands.ts +6 -0
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +10 -4
- 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 +116 -34
- package/src/capabilities/doctor/doctor.test.ts +31 -1
- package/src/capabilities/doctor/runChecks.ts +28 -4
- package/src/capabilities/doctor/types.ts +15 -0
- package/src/capabilities/ontology/index.ts +10 -3
- package/src/capabilities/ontology/ontology.test.ts +157 -5
- package/src/capabilities/ontology/verbs.ts +192 -93
- 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 +61 -0
- package/src/capabilities/reference.test.ts +32 -0
- package/src/capabilities/resources/provider.ts +28 -14
- package/src/capabilities/resources/resources.test.ts +99 -0
- 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 +157 -61
- 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 +7 -1
- 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 +57 -5
- package/src/capabilities/setup/types.ts +59 -2
- package/src/capabilities/shared/bands.ts +16 -0
- package/src/capabilities/sources/collectStatus.ts +19 -4
- package/src/capabilities/sources/runUpdate.ts +58 -1
- package/src/capabilities/sources/sources.test.ts +187 -1
- package/src/capabilities/sources/status.render.test.ts +65 -0
- package/src/capabilities/sources/status.render.ts +50 -23
- package/src/capabilities/surface.test.ts +34 -9
- package/src/capabilities/tier/lookup.render.test.ts +72 -0
- package/src/capabilities/tier/lookup.render.ts +33 -8
- package/src/capabilities/tier/lookup.test.ts +15 -0
- package/src/capabilities/tier/runLookup.ts +28 -0
- package/src/constants.test.ts +1 -1
- package/src/constants.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +13 -14
- package/src/kernel/completion/bashDrive.test.ts +3 -3
- package/src/kernel/completion/complete.test.ts +3 -5
- package/src/kernel/completion/entitySource.ts +14 -6
- package/src/kernel/completion/model.test.ts +2 -2
- package/src/kernel/completion/model.ts +0 -1
- package/src/kernel/completion/parse.test.ts +2 -4
- package/src/kernel/error/PragmaError.test.ts +4 -4
- package/src/kernel/error/renderError.ts +1 -1
- package/src/kernel/packs/compile.ts +1 -1
- package/src/kernel/project/cli/buildProgram.test.ts +51 -7
- package/src/kernel/project/cli/buildProgram.ts +11 -0
- package/src/kernel/project/cli/globalFlags.test.ts +12 -4
- package/src/kernel/project/cli/globalFlags.ts +14 -14
- package/src/kernel/project/cli/rootHelp.ts +4 -2
- package/src/kernel/project/mcp/instructions.ts +1 -1
- package/src/kernel/project/mcp/mcpError.ts +52 -0
- package/src/kernel/project/mcp/prompts/prompts.test.ts +8 -0
- package/src/kernel/project/mcp/prompts/provider.ts +36 -6
- package/src/kernel/render/formatters.ts +5 -4
- package/src/kernel/render/renderers.test.ts +34 -0
- 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 +28 -0
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +103 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +124 -36
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +3 -6
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -2
- package/src/kernel/runtime/graphpack/graphpack.test.ts +69 -1
- package/src/kernel/runtime/graphpack/read.ts +28 -13
- package/src/kernel/runtime/graphpack/types.ts +14 -0
- package/src/kernel/runtime/loadSession.ts +4 -12
- package/src/kernel/runtime/refs/resolve.ts +63 -3
- package/src/kernel/runtime/storeReadiness.ts +61 -0
- package/src/kernel/runtime/types.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +3 -1
- package/src/kernel/spec/emitReference.test.ts +196 -0
- package/src/kernel/spec/emitReference.ts +491 -0
- package/src/kernel/spec/emitSurface.ts +4 -2
- package/src/kernel/spec/index.ts +2 -0
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/testing/PARITY_GAPS.ts +1 -1
- package/src/testing/behavioral/autoLlm.e2e.test.ts +12 -2
- package/src/testing/behavioral/completion.test.ts +2 -2
- package/src/testing/behavioral/disclosure.test.ts +12 -0
- package/src/testing/behavioral/docExamples.test.ts +283 -0
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +2 -1
- package/src/testing/behavioral/journeys.cli.test.ts +3 -1
- package/src/testing/behavioral/journeys.defaultPack.test.ts +571 -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 +176 -0
- package/src/testing/behavioral/mcpSurface.test.ts +1 -1
- package/src/testing/behavioral/staleCommands.test.ts +76 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +2 -2
- package/src/testing/eval/cases/assembly.ts +3 -3
- package/src/testing/eval/cases/effectful.ts +6 -3
- package/src/testing/eval/cases/readNouns.ts +4 -1
- package/src/testing/fixtures/fixtureCapability.ts +156 -0
- package/src/testing/fixtures/graph/defaultPack.ts +178 -0
- package/src/testing/fixtures/graph/owlExport.ts +95 -0
- package/src/testing/helpers/extractSnippets.ts +29 -0
- package/src/testing/helpers/golden.ts +1 -1
- package/src/capabilities/config/field.test.ts +0 -188
|
@@ -1,35 +1,74 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Formatters for `pragma doctor` — plain (chalk), llm (Markdown), json.
|
|
2
|
+
* Formatters for `pragma doctor` — plain (chalk on a TTY), llm (Markdown), json.
|
|
3
3
|
*
|
|
4
4
|
* Ported from the old shell's `doctor/formatters/doctor.ts`, retargeted at the
|
|
5
|
-
* kernel `Formatters` contract.
|
|
5
|
+
* kernel `Formatters` contract. The plain path is colored ONLY on a color-capable
|
|
6
|
+
* TTY: it consults the shared {@link defaultStyle} seam (stdout `isTTY` AND a
|
|
7
|
+
* non-zero chalk level), so a piped / redirected / CI run — where `supports-color`
|
|
8
|
+
* can report a level with no TTY (`GITHUB_ACTIONS`, `FORCE_COLOR`) — renders the
|
|
9
|
+
* plain form byte-for-byte instead of leaking ANSI into `doctor --format plain`.
|
|
10
|
+
* Glyphs are plain constants tinted at render time; nothing is baked at load.
|
|
6
11
|
*/
|
|
7
12
|
|
|
8
13
|
import chalk from "chalk";
|
|
14
|
+
import { defaultStyle, type RenderStyle } from "../../kernel/render/style.js";
|
|
9
15
|
import type { Formatters } from "../../kernel/spec/types.js";
|
|
16
|
+
import { BAND_LABELS } from "../shared/bands.js";
|
|
10
17
|
import type { CheckResult, CheckStatus, DoctorData } from "./types.js";
|
|
11
18
|
|
|
12
|
-
/**
|
|
13
|
-
const
|
|
14
|
-
pass:
|
|
15
|
-
fail:
|
|
16
|
-
skip:
|
|
19
|
+
/** Uncolored status glyphs — tinted at render time (never baked at module load). */
|
|
20
|
+
const STATUS_GLYPHS: Record<CheckStatus, string> = {
|
|
21
|
+
pass: "✓",
|
|
22
|
+
fail: "✗",
|
|
23
|
+
skip: "○",
|
|
17
24
|
};
|
|
18
25
|
|
|
19
|
-
const SUB_BULLET =
|
|
20
|
-
const FIX_ARROW =
|
|
26
|
+
const SUB_BULLET = "·";
|
|
27
|
+
const FIX_ARROW = "↳";
|
|
21
28
|
const INDENT = " "; // aligns sub-lines under the check name
|
|
22
29
|
|
|
30
|
+
/** The TTY styler plus `red` — the one tint {@link RenderStyle} does not carry. */
|
|
31
|
+
interface DoctorStyle extends RenderStyle {
|
|
32
|
+
red(text: string): string;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Build the doctor styler for this process's stdout: the shared TTY seam, plus a
|
|
37
|
+
* `red` gated on the SAME decision, so a fail tint appears only on a color TTY.
|
|
38
|
+
*
|
|
39
|
+
* @returns A colorizing styler on a color-capable TTY, else the identity styler.
|
|
40
|
+
* @note Impure — {@link defaultStyle} reads `process.stdout.isTTY` + chalk level.
|
|
41
|
+
*/
|
|
42
|
+
function doctorStyle(): DoctorStyle {
|
|
43
|
+
const style = defaultStyle();
|
|
44
|
+
return {
|
|
45
|
+
...style,
|
|
46
|
+
red: style.enabled ? (text) => chalk.red(text) : (text) => text,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** Tint a status glyph by meaning — green pass, red fail, yellow skip. */
|
|
51
|
+
function paintGlyph(status: CheckStatus, style: DoctorStyle): string {
|
|
52
|
+
const glyph = STATUS_GLYPHS[status];
|
|
53
|
+
if (status === "pass") return style.green(glyph);
|
|
54
|
+
if (status === "fail") return style.red(glyph);
|
|
55
|
+
return style.yellow(glyph);
|
|
56
|
+
}
|
|
57
|
+
|
|
23
58
|
/**
|
|
24
59
|
* Render one check as terminal lines: a headline row (icon, aligned name,
|
|
25
60
|
* detail), an optional indented breakdown of sub-items, and — for failures — an
|
|
26
61
|
* inline remedial instruction.
|
|
27
62
|
*/
|
|
28
|
-
function formatCheckPlain(
|
|
63
|
+
function formatCheckPlain(
|
|
64
|
+
check: CheckResult,
|
|
65
|
+
nameWidth: number,
|
|
66
|
+
style: DoctorStyle,
|
|
67
|
+
): string[] {
|
|
29
68
|
const name = check.name.padEnd(nameWidth);
|
|
30
|
-
const label = check.status === "fail" ?
|
|
69
|
+
const label = check.status === "fail" ? style.red(name) : style.bold(name);
|
|
31
70
|
const lines = [
|
|
32
|
-
` ${
|
|
71
|
+
` ${paintGlyph(check.status, style)} ${label} ${style.dim(check.detail)}`,
|
|
33
72
|
];
|
|
34
73
|
|
|
35
74
|
if (check.items && check.items.length > 0) {
|
|
@@ -38,55 +77,86 @@ function formatCheckPlain(check: CheckResult, nameWidth: number): string[] {
|
|
|
38
77
|
for (const item of check.items) {
|
|
39
78
|
const icon = anyStatus
|
|
40
79
|
? item.status
|
|
41
|
-
? `${
|
|
80
|
+
? `${paintGlyph(item.status, style)} `
|
|
42
81
|
: " "
|
|
43
82
|
: "";
|
|
44
83
|
const itemLabel = item.detail ? item.label.padEnd(itemWidth) : item.label;
|
|
45
|
-
const detail = item.detail ? ` ${
|
|
46
|
-
lines.push(
|
|
84
|
+
const detail = item.detail ? ` ${style.dim(item.detail)}` : "";
|
|
85
|
+
lines.push(
|
|
86
|
+
`${INDENT}${style.dim(SUB_BULLET)} ${icon}${itemLabel}${detail}`,
|
|
87
|
+
);
|
|
47
88
|
}
|
|
48
89
|
}
|
|
49
90
|
|
|
50
91
|
if (check.status === "fail" && check.remedy) {
|
|
51
|
-
lines.push(
|
|
92
|
+
lines.push(
|
|
93
|
+
`${INDENT}${style.cyan(FIX_ARROW)} ${style.cyan("fix:")} ${check.remedy}`,
|
|
94
|
+
);
|
|
52
95
|
}
|
|
53
96
|
|
|
54
97
|
return lines;
|
|
55
98
|
}
|
|
56
99
|
|
|
57
100
|
/** Render the pass/fail/skip tally, coloring non-zero fail/skip. */
|
|
58
|
-
function formatSummary(data: DoctorData): string {
|
|
59
|
-
const parts = [
|
|
101
|
+
function formatSummary(data: DoctorData, style: DoctorStyle): string {
|
|
102
|
+
const parts = [style.green(`${data.passed} passed`)];
|
|
60
103
|
parts.push(
|
|
61
104
|
data.failed > 0
|
|
62
|
-
?
|
|
63
|
-
:
|
|
105
|
+
? style.red(`${data.failed} failed`)
|
|
106
|
+
: style.dim(`${data.failed} failed`),
|
|
64
107
|
);
|
|
65
108
|
parts.push(
|
|
66
109
|
data.skipped > 0
|
|
67
|
-
?
|
|
68
|
-
:
|
|
110
|
+
? style.yellow(`${data.skipped} skipped`)
|
|
111
|
+
: style.dim(`${data.skipped} skipped`),
|
|
69
112
|
);
|
|
70
|
-
return ` ${parts.join(
|
|
113
|
+
return ` ${parts.join(style.dim(" · "))}`;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Partition checks into ordered bands: environment (no band), then the global
|
|
118
|
+
* and project bands. Declaration order is preserved within each band, so the
|
|
119
|
+
* report stays deterministic.
|
|
120
|
+
*/
|
|
121
|
+
function partitionByBand(checks: readonly CheckResult[]): {
|
|
122
|
+
environment: CheckResult[];
|
|
123
|
+
machine: CheckResult[];
|
|
124
|
+
project: CheckResult[];
|
|
125
|
+
} {
|
|
126
|
+
return {
|
|
127
|
+
environment: checks.filter((c) => c.band === undefined),
|
|
128
|
+
machine: checks.filter((c) => c.band === "global"),
|
|
129
|
+
project: checks.filter((c) => c.band === "project"),
|
|
130
|
+
};
|
|
71
131
|
}
|
|
72
132
|
|
|
73
133
|
export const doctorFormatters: Formatters<DoctorData> = {
|
|
74
134
|
plain(data) {
|
|
135
|
+
const style = doctorStyle();
|
|
75
136
|
const nameWidth = Math.max(...data.checks.map((c) => c.name.length), 0);
|
|
76
|
-
const lines: string[] = [
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
137
|
+
const lines: string[] = [style.bold("pragma doctor"), ""];
|
|
138
|
+
const { environment, machine, project } = partitionByBand(data.checks);
|
|
139
|
+
const section = (heading: string, checks: CheckResult[]): void => {
|
|
140
|
+
if (checks.length === 0) return;
|
|
141
|
+
if (heading) lines.push(style.bold(heading));
|
|
142
|
+
for (const check of checks)
|
|
143
|
+
lines.push(...formatCheckPlain(check, nameWidth, style));
|
|
144
|
+
lines.push("");
|
|
145
|
+
};
|
|
146
|
+
// Environment checks lead (no header); the global then project bands are the
|
|
147
|
+
// two banded sections before the tally.
|
|
148
|
+
section("", environment);
|
|
149
|
+
section(BAND_LABELS.global, machine);
|
|
150
|
+
section(BAND_LABELS.project, project);
|
|
151
|
+
lines.push(formatSummary(data, style));
|
|
81
152
|
return lines.join("\n");
|
|
82
153
|
},
|
|
83
154
|
|
|
84
155
|
llm(data) {
|
|
85
|
-
const
|
|
86
|
-
for (const check of data.checks) {
|
|
156
|
+
const renderCheck = (check: CheckResult): string[] => {
|
|
87
157
|
const icon =
|
|
88
158
|
check.status === "pass" ? "✓" : check.status === "fail" ? "✗" : "○";
|
|
89
|
-
|
|
159
|
+
const out = [`- ${icon} **${check.name}**: ${check.detail}`];
|
|
90
160
|
for (const item of check.items ?? []) {
|
|
91
161
|
const itemIcon =
|
|
92
162
|
item.status === "pass"
|
|
@@ -97,12 +167,24 @@ export const doctorFormatters: Formatters<DoctorData> = {
|
|
|
97
167
|
? "○ "
|
|
98
168
|
: "";
|
|
99
169
|
const detail = item.detail ? `: ${item.detail}` : "";
|
|
100
|
-
|
|
170
|
+
out.push(` - ${itemIcon}${item.label}${detail}`);
|
|
101
171
|
}
|
|
102
172
|
if (check.status === "fail" && check.remedy) {
|
|
103
|
-
|
|
173
|
+
out.push(` - _fix:_ \`${check.remedy}\``);
|
|
104
174
|
}
|
|
105
|
-
|
|
175
|
+
return out;
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
const lines: string[] = ["## Doctor", ""];
|
|
179
|
+
const { environment, machine, project } = partitionByBand(data.checks);
|
|
180
|
+
const section = (heading: string, checks: CheckResult[]): void => {
|
|
181
|
+
if (checks.length === 0) return;
|
|
182
|
+
if (heading) lines.push(`### ${heading}`, "");
|
|
183
|
+
for (const check of checks) lines.push(...renderCheck(check));
|
|
184
|
+
};
|
|
185
|
+
section("", environment);
|
|
186
|
+
section(BAND_LABELS.global, machine);
|
|
187
|
+
section(BAND_LABELS.project, project);
|
|
106
188
|
lines.push(
|
|
107
189
|
"",
|
|
108
190
|
`_${data.passed} passed, ${data.failed} failed, ${data.skipped} skipped_`,
|
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* the MCP read-only envelope.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { mkdtempSync, rmSync } from "node:fs";
|
|
11
|
+
import { mkdirSync, mkdtempSync, rmSync, writeFileSync } from "node:fs";
|
|
12
12
|
import { tmpdir } from "node:os";
|
|
13
13
|
import { join } from "node:path";
|
|
14
14
|
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
@@ -28,6 +28,7 @@ import {
|
|
|
28
28
|
} from "../../testing/fixtures/graph/canonical.js";
|
|
29
29
|
import { bootFixtureRuntime } from "../../testing/helpers/fixtureGraph.js";
|
|
30
30
|
import { projectMcp } from "../../testing/helpers/projectMcp.js";
|
|
31
|
+
import { checkMcpConfigured } from "./checks/checkMcpConfigured.js";
|
|
31
32
|
import { doctorModule } from "./index.js";
|
|
32
33
|
import { runChecks } from "./runChecks.js";
|
|
33
34
|
import type { DoctorData } from "./types.js";
|
|
@@ -50,17 +51,24 @@ const tmp = (prefix: string): string => {
|
|
|
50
51
|
|
|
51
52
|
let prevHome: string | undefined;
|
|
52
53
|
let prevXdg: string | undefined;
|
|
54
|
+
let prevPath: string | undefined;
|
|
53
55
|
beforeEach(() => {
|
|
54
56
|
prevHome = process.env.HOME;
|
|
55
57
|
prevXdg = process.env.XDG_CONFIG_HOME;
|
|
58
|
+
prevPath = process.env.PATH;
|
|
56
59
|
// Empty HOME/XDG so `~`-based harness signals, rc files, and the global config
|
|
57
60
|
// are all absent — the harness/config/completion checks become deterministic.
|
|
58
61
|
process.env.HOME = tmp("pragma-doctor-home-");
|
|
59
62
|
process.env.XDG_CONFIG_HOME = tmp("pragma-doctor-xdg-");
|
|
63
|
+
// Point PATH at an empty dir so no harness `process` signal fires off the
|
|
64
|
+
// ambient PATH (e.g. a `claude`/`codex` binary on the CI/dev host) — keeping
|
|
65
|
+
// "no harnesses detected" deterministic regardless of what is installed.
|
|
66
|
+
process.env.PATH = tmp("pragma-doctor-path-");
|
|
60
67
|
});
|
|
61
68
|
afterEach(() => {
|
|
62
69
|
process.env.HOME = prevHome;
|
|
63
70
|
process.env.XDG_CONFIG_HOME = prevXdg;
|
|
71
|
+
process.env.PATH = prevPath;
|
|
64
72
|
for (const dir of roots) rmSync(dir, { recursive: true, force: true });
|
|
65
73
|
roots.length = 0;
|
|
66
74
|
});
|
|
@@ -177,3 +185,25 @@ describe("doctor — dispatch & MCP", () => {
|
|
|
177
185
|
expect((envelope.data as DoctorData).checks).toHaveLength(9);
|
|
178
186
|
});
|
|
179
187
|
});
|
|
188
|
+
|
|
189
|
+
describe("doctor — MCP checks band by detected harness scope, not check name", () => {
|
|
190
|
+
it("bands a configured GLOBAL-scope harness (Windsurf) as global", async () => {
|
|
191
|
+
// Windsurf is a global-only harness whose MCP config lives in the home band.
|
|
192
|
+
// The old static map tagged every "MCP configured" result PROJECT; the fix
|
|
193
|
+
// derives the band from the harness's real scope, so this reports `global`.
|
|
194
|
+
const cwd = tmp("pragma-doctor-proj-");
|
|
195
|
+
mkdirSync(join(cwd, ".windsurf"), { recursive: true }); // ⇒ Windsurf detected
|
|
196
|
+
const home = process.env.HOME ?? "";
|
|
197
|
+
const wsDir = join(home, ".codeium", "windsurf");
|
|
198
|
+
mkdirSync(wsDir, { recursive: true });
|
|
199
|
+
writeFileSync(
|
|
200
|
+
join(wsDir, "mcp_config.json"),
|
|
201
|
+
JSON.stringify({ mcpServers: { pragma: { command: "pragma" } } }),
|
|
202
|
+
);
|
|
203
|
+
|
|
204
|
+
const check = await checkMcpConfigured(cwd);
|
|
205
|
+
expect(check.status).toBe("pass");
|
|
206
|
+
expect(check.detail).toContain("Windsurf");
|
|
207
|
+
expect(check.band).toBe("global"); // NOT the old static "project"
|
|
208
|
+
});
|
|
209
|
+
});
|
|
@@ -18,7 +18,31 @@ import { checkPackageRefs } from "./checks/checkPackageRefs.js";
|
|
|
18
18
|
import { checkPragmaVersion } from "./checks/checkPragmaVersion.js";
|
|
19
19
|
import { checkShellCompletions } from "./checks/checkShellCompletions.js";
|
|
20
20
|
import { checkSkillsSymlinked } from "./checks/checkSkillsSymlinked.js";
|
|
21
|
-
import type { CheckResult, DoctorData } from "./types.js";
|
|
21
|
+
import type { CheckResult, DoctorData, ScopeBand } from "./types.js";
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* The band a check statically concerns, by name — for checks whose band is
|
|
25
|
+
* fixed by construction: skills always symlink into the per-repo project dir
|
|
26
|
+
* (`skillsPath` is project-rooted), and completions install into the user/home
|
|
27
|
+
* shell dir. The MCP checks are NOT here: they detect harnesses across BOTH
|
|
28
|
+
* bands, so each derives its own band from what it found (see `deriveBand`) and
|
|
29
|
+
* sets it on its own result — {@link attachBand} preserves that.
|
|
30
|
+
*/
|
|
31
|
+
const CHECK_BANDS: Record<string, ScopeBand> = {
|
|
32
|
+
"Skills symlinked": "project",
|
|
33
|
+
"Shell completions": "global",
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Attach a band tag to a check result. A check that already set its own band
|
|
38
|
+
* (the harness-derived MCP checks) keeps it; otherwise the static
|
|
39
|
+
* {@link CHECK_BANDS} map fills one in by name, if defined.
|
|
40
|
+
*/
|
|
41
|
+
function attachBand(result: CheckResult): CheckResult {
|
|
42
|
+
if (result.band !== undefined) return result;
|
|
43
|
+
const band = CHECK_BANDS[result.name];
|
|
44
|
+
return band ? { ...result, band } : result;
|
|
45
|
+
}
|
|
22
46
|
|
|
23
47
|
/**
|
|
24
48
|
* Each check paired with the display name used if it rejects. The fallback name
|
|
@@ -53,16 +77,16 @@ export async function runChecks(rt: PragmaRuntime): Promise<DoctorData> {
|
|
|
53
77
|
const checks: CheckResult[] = await Promise.all(
|
|
54
78
|
buildChecks(rt).map(async ([name, promise]): Promise<CheckResult> => {
|
|
55
79
|
try {
|
|
56
|
-
return await promise;
|
|
80
|
+
return attachBand(await promise);
|
|
57
81
|
} catch (error) {
|
|
58
82
|
const reason = error instanceof Error ? error.message : String(error);
|
|
59
|
-
return {
|
|
83
|
+
return attachBand({
|
|
60
84
|
name,
|
|
61
85
|
status: "fail",
|
|
62
86
|
detail: `Check threw an unexpected error: ${reason}`,
|
|
63
87
|
remedy:
|
|
64
88
|
"Re-run `pragma doctor`; if it persists, report this as a bug.",
|
|
65
|
-
};
|
|
89
|
+
});
|
|
66
90
|
}
|
|
67
91
|
}),
|
|
68
92
|
);
|
|
@@ -2,6 +2,13 @@
|
|
|
2
2
|
* Data shapes for `pragma doctor` (ported verbatim from the old shell).
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
/**
|
|
6
|
+
* One of the two config bands a check can concern. Mirrors the harnesses
|
|
7
|
+
* `ScopeBand` structurally but is redeclared here so this statically-reachable
|
|
8
|
+
* type module never pulls the harnesses runtime into the fast-path module graph.
|
|
9
|
+
*/
|
|
10
|
+
export type ScopeBand = "project" | "global";
|
|
11
|
+
|
|
5
12
|
/** Status of a doctor check or one of its sub-items. */
|
|
6
13
|
export type CheckStatus = "pass" | "fail" | "skip";
|
|
7
14
|
|
|
@@ -30,6 +37,14 @@ export interface CheckResult {
|
|
|
30
37
|
readonly items?: readonly CheckItem[];
|
|
31
38
|
/** Remedial instruction shown inline under a failing check. */
|
|
32
39
|
readonly remedy?: string;
|
|
40
|
+
/**
|
|
41
|
+
* Which config band the check concerns, if any: `global` for the user/home
|
|
42
|
+
* level (shell completions), `project` for per-repo config (skills). The MCP
|
|
43
|
+
* checks derive their band from the harnesses they found. The renderer groups
|
|
44
|
+
* banded checks into Global/Project sections; environment checks (Node,
|
|
45
|
+
* versions, store) carry no band.
|
|
46
|
+
*/
|
|
47
|
+
readonly band?: ScopeBand;
|
|
33
48
|
}
|
|
34
49
|
|
|
35
50
|
/** Aggregated results from all doctor checks. */
|
|
@@ -3,10 +3,17 @@
|
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
5
|
import type { CapabilityModule } from "../../kernel/spec/types.js";
|
|
6
|
-
import {
|
|
6
|
+
import {
|
|
7
|
+
ontologyListVerb,
|
|
8
|
+
ontologyLookupVerb,
|
|
9
|
+
ontologyShowVerb,
|
|
10
|
+
} from "./verbs.js";
|
|
7
11
|
|
|
8
|
-
/**
|
|
12
|
+
/**
|
|
13
|
+
* The `ontology` capability module (`list`, `lookup`, and the deprecated `show`
|
|
14
|
+
* alias — AV-228 B1).
|
|
15
|
+
*/
|
|
9
16
|
export const ontologyModule: CapabilityModule = {
|
|
10
17
|
name: "ontology",
|
|
11
|
-
verbs: [ontologyListVerb, ontologyShowVerb],
|
|
18
|
+
verbs: [ontologyListVerb, ontologyLookupVerb, ontologyShowVerb],
|
|
12
19
|
};
|
|
@@ -15,13 +15,23 @@ import {
|
|
|
15
15
|
BLOCK_PREFIXES,
|
|
16
16
|
BLOCK_TTL,
|
|
17
17
|
} from "../../testing/fixtures/blockGraph.js";
|
|
18
|
+
import { CANONICAL_TTL } from "../../testing/fixtures/graph/canonical.js";
|
|
19
|
+
import {
|
|
20
|
+
bootFixtureRuntime,
|
|
21
|
+
type FixtureGraph,
|
|
22
|
+
} from "../../testing/helpers/fixtureGraph.js";
|
|
18
23
|
import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
|
|
24
|
+
import { projectMcp } from "../../testing/helpers/projectMcp.js";
|
|
25
|
+
import { capabilities } from "../index.js";
|
|
19
26
|
import { ontologyModule } from "./index.js";
|
|
20
27
|
import type { OntologyShowData, OntologySummary } from "./queries.js";
|
|
21
28
|
|
|
22
29
|
const listVerb = ontologyModule.verbs.find(
|
|
23
30
|
(v) => verbKey(v.path) === "ontology list",
|
|
24
31
|
) as VerbSpec;
|
|
32
|
+
const lookupVerb = ontologyModule.verbs.find(
|
|
33
|
+
(v) => verbKey(v.path) === "ontology lookup",
|
|
34
|
+
) as VerbSpec;
|
|
25
35
|
const showVerb = ontologyModule.verbs.find(
|
|
26
36
|
(v) => verbKey(v.path) === "ontology show",
|
|
27
37
|
) as VerbSpec;
|
|
@@ -48,9 +58,12 @@ describe("ontology list", () => {
|
|
|
48
58
|
});
|
|
49
59
|
});
|
|
50
60
|
|
|
51
|
-
describe("ontology
|
|
61
|
+
describe("ontology lookup (primary by-name read)", () => {
|
|
52
62
|
it("renders the class hierarchy with per-class instance counts from the index", async () => {
|
|
53
|
-
const data = (await
|
|
63
|
+
const data = (await lookupVerb.run(
|
|
64
|
+
{ prefix: "ds" },
|
|
65
|
+
rt,
|
|
66
|
+
)) as OntologyShowData;
|
|
54
67
|
const component = data.classes.find((c) => c.uri.endsWith("Component"));
|
|
55
68
|
expect(component?.superclass).toBe("https://ds.canonical.com/UIBlock");
|
|
56
69
|
// Two Component individuals (Button, Modal) in the fixture.
|
|
@@ -58,8 +71,34 @@ describe("ontology show", () => {
|
|
|
58
71
|
expect(data.properties).toEqual([]);
|
|
59
72
|
});
|
|
60
73
|
|
|
74
|
+
it("--detail detailed folds onto disclosure and includes properties (B5)", async () => {
|
|
75
|
+
// Ontology honours the canonical `--detail` instead of only its bespoke
|
|
76
|
+
// `--properties`: standard/detailed add the properties section.
|
|
77
|
+
const detailed = {
|
|
78
|
+
...rt,
|
|
79
|
+
globalFlags: { ...rt.globalFlags, detail: "detailed" as const },
|
|
80
|
+
};
|
|
81
|
+
const data = (await lookupVerb.run(
|
|
82
|
+
{ prefix: "ds" },
|
|
83
|
+
detailed,
|
|
84
|
+
)) as OntologyShowData;
|
|
85
|
+
expect(data.properties.length).toBeGreaterThan(0);
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
it("--detail summary keeps classes only (no properties)", async () => {
|
|
89
|
+
const summary = {
|
|
90
|
+
...rt,
|
|
91
|
+
globalFlags: { ...rt.globalFlags, detail: "summary" as const },
|
|
92
|
+
};
|
|
93
|
+
const data = (await lookupVerb.run(
|
|
94
|
+
{ prefix: "ds" },
|
|
95
|
+
summary,
|
|
96
|
+
)) as OntologyShowData;
|
|
97
|
+
expect(data.properties).toEqual([]);
|
|
98
|
+
});
|
|
99
|
+
|
|
61
100
|
it("--class focuses on one class and its properties", async () => {
|
|
62
|
-
const data = (await
|
|
101
|
+
const data = (await lookupVerb.run(
|
|
63
102
|
{ prefix: "ds", class: "BlockProperty" },
|
|
64
103
|
rt,
|
|
65
104
|
)) as OntologyShowData;
|
|
@@ -77,18 +116,131 @@ describe("ontology show", () => {
|
|
|
77
116
|
});
|
|
78
117
|
|
|
79
118
|
it("--full-uris flag rides on the payload for the renderer", async () => {
|
|
80
|
-
const data = (await
|
|
119
|
+
const data = (await lookupVerb.run(
|
|
81
120
|
{ prefix: "ds", fullUris: true },
|
|
82
121
|
rt,
|
|
83
122
|
)) as OntologyShowData;
|
|
84
123
|
expect(data.fullUris).toBe(true);
|
|
85
|
-
const llm =
|
|
124
|
+
const llm = lookupVerb.output.formatters.llm(data);
|
|
86
125
|
expect(llm).toContain("https://ds.canonical.com/Component");
|
|
87
126
|
});
|
|
88
127
|
|
|
89
128
|
it("rejects an unknown prefix with recovery", async () => {
|
|
129
|
+
await expect(lookupVerb.run({ prefix: "nope" }, rt)).rejects.toThrow(
|
|
130
|
+
/prefix/i,
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe("ontology show — deprecated alias of lookup (AV-228 B1)", () => {
|
|
136
|
+
it("is marked deprecated in its summary and steers callers to `ontology lookup`", () => {
|
|
137
|
+
expect(showVerb.summary.toLowerCase()).toContain("deprecated");
|
|
138
|
+
expect(showVerb.summary).toContain("ontology lookup");
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it("shares the lookup handler — same payload for the same input", async () => {
|
|
142
|
+
const viaLookup = (await lookupVerb.run(
|
|
143
|
+
{ prefix: "ds", properties: true },
|
|
144
|
+
rt,
|
|
145
|
+
)) as OntologyShowData;
|
|
146
|
+
const viaShow = (await showVerb.run(
|
|
147
|
+
{ prefix: "ds", properties: true },
|
|
148
|
+
rt,
|
|
149
|
+
)) as OntologyShowData;
|
|
150
|
+
expect(viaShow).toEqual(viaLookup);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("still rejects an unknown prefix (the alias is a full, callable verb)", async () => {
|
|
90
154
|
await expect(showVerb.run({ prefix: "nope" }, rt)).rejects.toThrow(
|
|
91
155
|
/prefix/i,
|
|
92
156
|
);
|
|
93
157
|
});
|
|
94
158
|
});
|
|
159
|
+
|
|
160
|
+
describe("ontology_show honours detail over MCP (B5)", () => {
|
|
161
|
+
// The CLI B5 cases above seed `globalFlags.detail` and call `showVerb.run`
|
|
162
|
+
// directly. Over MCP there are NO global flags — the ONLY detail channel is
|
|
163
|
+
// the per-tool `detail` param the projector injects from the VerbSpec
|
|
164
|
+
// disclosure. Under an AMBIENT config `detail: standard`, an `ontology_show`
|
|
165
|
+
// with no declared disclosure would force the properties section with no
|
|
166
|
+
// per-call escape (the AV-228 MCP-opt-out asymmetry `block`/`standard` lack);
|
|
167
|
+
// declaring disclosure lights the param up, so an agent can ask for
|
|
168
|
+
// classes-only per call and can equally override upward.
|
|
169
|
+
let fixture: FixtureGraph;
|
|
170
|
+
let mcp: Awaited<ReturnType<typeof projectMcp>>;
|
|
171
|
+
|
|
172
|
+
beforeAll(async () => {
|
|
173
|
+
// Ambient config detail=standard → origins.detail === "project", so it
|
|
174
|
+
// outranks the verb's `summary` default inside resolvePackDetail.
|
|
175
|
+
fixture = await bootFixtureRuntime({
|
|
176
|
+
ttl: CANONICAL_TTL,
|
|
177
|
+
config: { detail: "standard" },
|
|
178
|
+
});
|
|
179
|
+
mcp = await projectMcp(capabilities, fixture.cwd);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
afterAll(async () => {
|
|
183
|
+
await mcp.cleanup();
|
|
184
|
+
await fixture.dispose();
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it("injects a detail param onto the ontology_show tool (symmetric with block/standard)", async () => {
|
|
188
|
+
const tools = await mcp.listTools();
|
|
189
|
+
const show = tools.find((t) => t.name === "ontology_show");
|
|
190
|
+
const schema = show?.inputSchema as {
|
|
191
|
+
properties?: Record<string, unknown>;
|
|
192
|
+
};
|
|
193
|
+
expect(schema.properties?.detail).toBeDefined();
|
|
194
|
+
});
|
|
195
|
+
|
|
196
|
+
it("exposes the primary ontology_lookup tool with the same injected detail param (AV-228 B1)", async () => {
|
|
197
|
+
const tools = await mcp.listTools();
|
|
198
|
+
const lookup = tools.find((t) => t.name === "ontology_lookup");
|
|
199
|
+
expect(lookup).toBeDefined();
|
|
200
|
+
const schema = lookup?.inputSchema as {
|
|
201
|
+
properties?: Record<string, unknown>;
|
|
202
|
+
};
|
|
203
|
+
expect(schema.properties?.detail).toBeDefined();
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
it("ontology_lookup honours a per-call detail=summary despite the ambient standard", async () => {
|
|
207
|
+
const result = await mcp.callTool("ontology_lookup", {
|
|
208
|
+
prefix: "ds",
|
|
209
|
+
detail: "summary",
|
|
210
|
+
});
|
|
211
|
+
expect(result.ok).toBe(true);
|
|
212
|
+
const data = result.data as { classes: unknown[]; properties: unknown[] };
|
|
213
|
+
expect(data.classes.length).toBeGreaterThan(0);
|
|
214
|
+
expect(data.properties).toEqual([]);
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
it("honours the ambient detail=standard config when no per-call detail is set", async () => {
|
|
218
|
+
// Proves the ambient level really is standard, so the summary case below is
|
|
219
|
+
// a genuine per-call override rather than merely the spec default.
|
|
220
|
+
const result = await mcp.callTool("ontology_show", { prefix: "ds" });
|
|
221
|
+
expect(result.ok).toBe(true);
|
|
222
|
+
const data = result.data as { properties: unknown[] };
|
|
223
|
+
expect(data.properties.length).toBeGreaterThan(0);
|
|
224
|
+
});
|
|
225
|
+
|
|
226
|
+
it("a per-call detail=summary returns classes-only despite the ambient standard", async () => {
|
|
227
|
+
const result = await mcp.callTool("ontology_show", {
|
|
228
|
+
prefix: "ds",
|
|
229
|
+
detail: "summary",
|
|
230
|
+
});
|
|
231
|
+
expect(result.ok).toBe(true);
|
|
232
|
+
const data = result.data as { classes: unknown[]; properties: unknown[] };
|
|
233
|
+
expect(data.classes.length).toBeGreaterThan(0);
|
|
234
|
+
expect(data.properties).toEqual([]);
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it("an explicit detail=detailed overrides upward to include properties", async () => {
|
|
238
|
+
const result = await mcp.callTool("ontology_show", {
|
|
239
|
+
prefix: "ds",
|
|
240
|
+
detail: "detailed",
|
|
241
|
+
});
|
|
242
|
+
expect(result.ok).toBe(true);
|
|
243
|
+
const data = result.data as { properties: unknown[] };
|
|
244
|
+
expect(data.properties.length).toBeGreaterThan(0);
|
|
245
|
+
});
|
|
246
|
+
});
|