@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,17 +1,36 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* `ontology list` and `ontology
|
|
2
|
+
* `ontology list` and `ontology lookup <prefix>` — schema (TBox) inspection.
|
|
3
3
|
*
|
|
4
|
-
* `list` groups loaded namespaces by prefix with class/property counts; `
|
|
4
|
+
* `list` groups loaded namespaces by prefix with class/property counts; `lookup`
|
|
5
5
|
* renders a namespace's class hierarchy (with per-class instance counts read
|
|
6
|
-
* from the pack index) and, with `--properties
|
|
7
|
-
* properties. `--full-uris` shows absolute IRIs;
|
|
8
|
-
* class and the properties whose domain is that
|
|
6
|
+
* from the pack index) and, with `--properties`, a `--class` focus, or a
|
|
7
|
+
* `--detail` of `standard`+, its properties. `--full-uris` shows absolute IRIs;
|
|
8
|
+
* `--class <name>` narrows to one class and the properties whose domain is that
|
|
9
|
+
* class.
|
|
10
|
+
*
|
|
11
|
+
* `ontology show` is the DEPRECATED alias of `ontology lookup` (AV-228 B1): the
|
|
12
|
+
* verb was renamed to `lookup` for consistency with block/standard/tier/etc.
|
|
13
|
+
* (the ratified by-name read name), and `show` is kept callable — both share the
|
|
14
|
+
* same params, disclosure, and handler, and both stay blessed in the covenant.
|
|
15
|
+
*
|
|
16
|
+
* Disclosure (B5): rather than reinvent its own scheme, the by-name read declares
|
|
17
|
+
* the canonical disclosure ladder and honours the `--detail`/config `detail` the
|
|
18
|
+
* rest of the CLI reads AND the injected `detail` tool param the MCP projector
|
|
19
|
+
* derives from that declaration — a two-behaviour fold where `summary` is
|
|
20
|
+
* classes-only and `standard`/`detailed` add the properties section. The frozen
|
|
21
|
+
* `--properties` flag stays as an explicit override (it forces the section at
|
|
22
|
+
* any level), so the covenant is untouched.
|
|
9
23
|
*/
|
|
10
24
|
|
|
11
25
|
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
26
|
+
import { resolvePackDetail } from "../../kernel/packs/disclosure.js";
|
|
12
27
|
import type { PragmaRuntime } from "../../kernel/runtime/types.js";
|
|
13
28
|
import { asVerb } from "../../kernel/spec/asVerb.js";
|
|
14
|
-
import type {
|
|
29
|
+
import type {
|
|
30
|
+
DisclosureSpec,
|
|
31
|
+
ParamSpec,
|
|
32
|
+
VerbSpec,
|
|
33
|
+
} from "../../kernel/spec/types.js";
|
|
15
34
|
import {
|
|
16
35
|
listNamespaces,
|
|
17
36
|
localName,
|
|
@@ -73,98 +92,178 @@ const listVerb: VerbSpec<Record<string, unknown>, OntologySummary[]> = {
|
|
|
73
92
|
},
|
|
74
93
|
};
|
|
75
94
|
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
name: "class",
|
|
101
|
-
doc: "Focus on one class and its properties.",
|
|
102
|
-
},
|
|
103
|
-
],
|
|
104
|
-
output: { formatters: ontologyShowFormatters },
|
|
105
|
-
examples: [
|
|
106
|
-
{ cmd: "pragma ontology show ds" },
|
|
107
|
-
{ cmd: "pragma ontology show ds --properties" },
|
|
108
|
-
{ cmd: "pragma ontology show ds --class Component" },
|
|
109
|
-
],
|
|
110
|
-
capability: {
|
|
111
|
-
needsStore: true,
|
|
112
|
-
mutates: false,
|
|
113
|
-
mcp: {
|
|
114
|
-
expose: true,
|
|
115
|
-
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
116
|
-
},
|
|
95
|
+
/**
|
|
96
|
+
* The canonical disclosure the by-name read folds onto (B5). Declared on both
|
|
97
|
+
* the primary {@link ontologyLookupVerb} and its deprecated {@link
|
|
98
|
+
* ontologyShowVerb} alias — so the MCP projector injects the `detail` enum tool
|
|
99
|
+
* param and its per-call `withDetail` seeding light up (symmetric with
|
|
100
|
+
* `block`/`standard`, which an inline resolve left dark over MCP) — AND read by
|
|
101
|
+
* `run` to resolve the effective level, so the advertised param and the fetched
|
|
102
|
+
* level share ONE source. The levels are the full canonical ladder; the fold is
|
|
103
|
+
* two-behaviour (`summary` = classes only, `standard`/`detailed` add properties).
|
|
104
|
+
*/
|
|
105
|
+
const BY_NAME_DISCLOSURE: DisclosureSpec = {
|
|
106
|
+
levels: ["summary", "standard", "detailed"],
|
|
107
|
+
default: "summary",
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
/** The params shared by `ontology lookup` and its deprecated `ontology show` alias. */
|
|
111
|
+
const BY_NAME_PARAMS: ParamSpec[] = [
|
|
112
|
+
{
|
|
113
|
+
kind: "string",
|
|
114
|
+
name: "prefix",
|
|
115
|
+
doc: "The namespace prefix (ds) or full URI.",
|
|
116
|
+
positional: true,
|
|
117
|
+
required: true,
|
|
118
|
+
complete: { kind: "names", source: { from: "prefixes" } },
|
|
117
119
|
},
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
120
|
+
{
|
|
121
|
+
kind: "boolean",
|
|
122
|
+
name: "properties",
|
|
123
|
+
doc: "Include the properties section (also implied by --detail standard or higher).",
|
|
124
|
+
},
|
|
125
|
+
{
|
|
126
|
+
kind: "boolean",
|
|
127
|
+
name: "fullUris",
|
|
128
|
+
doc: "Show full IRIs instead of prefixed.",
|
|
129
|
+
},
|
|
130
|
+
{
|
|
131
|
+
kind: "string",
|
|
132
|
+
name: "class",
|
|
133
|
+
doc: "Focus on one class and its properties.",
|
|
134
|
+
},
|
|
135
|
+
];
|
|
136
|
+
|
|
137
|
+
/** The by-name read capability — a storeless-free, read-only MCP tool. */
|
|
138
|
+
const BY_NAME_CAPABILITY = {
|
|
139
|
+
needsStore: true,
|
|
140
|
+
mutates: false,
|
|
141
|
+
mcp: {
|
|
142
|
+
expose: true as const,
|
|
143
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
144
|
+
},
|
|
145
|
+
};
|
|
126
146
|
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
147
|
+
/**
|
|
148
|
+
* The by-name read handler shared by `ontology lookup` and `ontology show`.
|
|
149
|
+
*
|
|
150
|
+
* @param params - The coerced param bag (`prefix`, `properties`, `fullUris`, `class`).
|
|
151
|
+
* @param rt - The runtime (store + disclosure precedence).
|
|
152
|
+
* @returns The namespace's classes (and, per disclosure, properties) payload.
|
|
153
|
+
*/
|
|
154
|
+
async function runByName(
|
|
155
|
+
params: Record<string, unknown>,
|
|
156
|
+
rt: PragmaRuntime,
|
|
157
|
+
): Promise<OntologyShowData> {
|
|
158
|
+
const session = await rt.store.get();
|
|
159
|
+
const { prefix, namespace } = resolvePrefix(
|
|
160
|
+
String(params.prefix),
|
|
161
|
+
session.prefixes,
|
|
162
|
+
);
|
|
163
|
+
const focus = typeof params.class === "string" ? params.class : undefined;
|
|
164
|
+
// Fold ontology's bespoke disclosure onto the canonical `--detail` (B5),
|
|
165
|
+
// resolving through the SAME {@link BY_NAME_DISCLOSURE} the verb declares (so
|
|
166
|
+
// the MCP `detail` param and its withDetail seeding resolve identically, not
|
|
167
|
+
// just the CLI `--detail` flag) and the SAME precedence packs use (flag >
|
|
168
|
+
// explicit config > default). The frozen `--properties` flag and a `--class`
|
|
169
|
+
// focus still force the section, so honouring the level is covenant-safe.
|
|
170
|
+
const level = await resolvePackDetail(rt, BY_NAME_DISCLOSURE);
|
|
171
|
+
const wantProperties =
|
|
172
|
+
params.properties === true || focus !== undefined || level !== "summary";
|
|
173
|
+
|
|
174
|
+
let classes = await queryClasses(
|
|
175
|
+
rt,
|
|
176
|
+
namespace,
|
|
177
|
+
session.index.instanceCountByType,
|
|
178
|
+
);
|
|
179
|
+
let properties = wantProperties ? await queryProperties(rt, namespace) : [];
|
|
180
|
+
|
|
181
|
+
if (focus !== undefined) {
|
|
182
|
+
const needle = focus.toLowerCase();
|
|
183
|
+
classes = classes.filter(
|
|
184
|
+
(c) =>
|
|
185
|
+
localName(c.uri).toLowerCase() === needle ||
|
|
186
|
+
c.label.toLowerCase() === needle,
|
|
187
|
+
);
|
|
188
|
+
const focusUris = new Set(classes.map((c) => c.uri));
|
|
189
|
+
properties = properties.filter(
|
|
190
|
+
(p) => p.domain !== undefined && focusUris.has(p.domain),
|
|
131
191
|
);
|
|
132
|
-
|
|
192
|
+
}
|
|
133
193
|
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
(p) => p.domain !== undefined && focusUris.has(p.domain),
|
|
144
|
-
);
|
|
145
|
-
}
|
|
194
|
+
if (classes.length === 0 && properties.length === 0) {
|
|
195
|
+
throw PragmaError.notFound("ontology", String(params.prefix), {
|
|
196
|
+
recovery: {
|
|
197
|
+
message: "List loaded ontologies.",
|
|
198
|
+
cli: "pragma ontology list",
|
|
199
|
+
mcp: { tool: "ontology_list" },
|
|
200
|
+
},
|
|
201
|
+
});
|
|
202
|
+
}
|
|
146
203
|
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
204
|
+
return {
|
|
205
|
+
prefix,
|
|
206
|
+
namespace,
|
|
207
|
+
classes,
|
|
208
|
+
properties,
|
|
209
|
+
fullUris: params.fullUris === true,
|
|
210
|
+
...(focus !== undefined ? { focus } : {}),
|
|
211
|
+
};
|
|
212
|
+
}
|
|
156
213
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
214
|
+
/**
|
|
215
|
+
* Build one by-name read verb (`lookup` primary or `show` deprecated alias) —
|
|
216
|
+
* both share {@link BY_NAME_PARAMS}, {@link BY_NAME_DISCLOSURE}, the formatters,
|
|
217
|
+
* and {@link runByName}; only the path label, summary, doc, and examples differ.
|
|
218
|
+
*/
|
|
219
|
+
function byNameVerb(
|
|
220
|
+
verb: "lookup" | "show",
|
|
221
|
+
summary: string,
|
|
222
|
+
examples: VerbSpec["examples"],
|
|
223
|
+
doc?: string,
|
|
224
|
+
): VerbSpec<Record<string, unknown>, OntologyShowData> {
|
|
225
|
+
return {
|
|
226
|
+
path: ["ontology", verb],
|
|
227
|
+
summary,
|
|
228
|
+
...(doc !== undefined ? { doc } : {}),
|
|
229
|
+
params: BY_NAME_PARAMS,
|
|
230
|
+
output: { formatters: ontologyShowFormatters },
|
|
231
|
+
examples,
|
|
232
|
+
disclosure: BY_NAME_DISCLOSURE,
|
|
233
|
+
capability: BY_NAME_CAPABILITY,
|
|
234
|
+
run: runByName,
|
|
235
|
+
};
|
|
236
|
+
}
|
|
167
237
|
|
|
168
|
-
/** The `ontology` verbs (`list`, `show`). */
|
|
238
|
+
/** The `ontology` verbs (`list`, `lookup`, and the deprecated `show` alias). */
|
|
169
239
|
export const ontologyListVerb = asVerb(listVerb);
|
|
170
|
-
|
|
240
|
+
|
|
241
|
+
/** `ontology lookup <prefix>` — the primary by-name schema read. */
|
|
242
|
+
export const ontologyLookupVerb = asVerb(
|
|
243
|
+
byNameVerb(
|
|
244
|
+
"lookup",
|
|
245
|
+
"Look up a namespace's classes (hierarchy + counts) and properties.",
|
|
246
|
+
[
|
|
247
|
+
{ cmd: "pragma ontology lookup ds" },
|
|
248
|
+
{ cmd: "pragma ontology lookup ds --properties" },
|
|
249
|
+
{ cmd: "pragma ontology lookup ds --class Component" },
|
|
250
|
+
],
|
|
251
|
+
),
|
|
252
|
+
);
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* `ontology show <prefix>` — DEPRECATED alias of {@link ontologyLookupVerb}
|
|
256
|
+
* (AV-228 B1). Kept callable and blessed in the covenant; shares the same
|
|
257
|
+
* handler, so it behaves identically. New callers should use `ontology lookup`.
|
|
258
|
+
*/
|
|
259
|
+
export const ontologyShowVerb = asVerb(
|
|
260
|
+
byNameVerb(
|
|
261
|
+
"show",
|
|
262
|
+
"(deprecated: use `ontology lookup`) Show a namespace's classes (hierarchy + counts) and properties.",
|
|
263
|
+
[
|
|
264
|
+
{ cmd: "pragma ontology lookup ds", note: "prefer `lookup`" },
|
|
265
|
+
{ cmd: "pragma ontology show ds", note: "deprecated alias" },
|
|
266
|
+
],
|
|
267
|
+
"Deprecated alias of `ontology lookup` — retained for compatibility. Prefer `ontology lookup <prefix>`.",
|
|
268
|
+
),
|
|
269
|
+
);
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import chalk from "chalk";
|
|
2
|
+
import { afterEach, describe, expect, it } from "vitest";
|
|
3
|
+
import {
|
|
4
|
+
promptListFormatters,
|
|
5
|
+
promptLookupFormatters,
|
|
6
|
+
} from "./prompt.render.js";
|
|
7
|
+
import type { PromptListData, PromptLookupData } from "./types.js";
|
|
8
|
+
|
|
9
|
+
/** Run `body` with stdout's `isTTY` forced to `value`, then restore it. */
|
|
10
|
+
function withStdoutTty(value: boolean | undefined, body: () => void): void {
|
|
11
|
+
const stream = process.stdout as { isTTY?: boolean };
|
|
12
|
+
const saved = stream.isTTY;
|
|
13
|
+
stream.isTTY = value;
|
|
14
|
+
try {
|
|
15
|
+
body();
|
|
16
|
+
} finally {
|
|
17
|
+
stream.isTTY = saved;
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
const LOOKUP: PromptLookupData = {
|
|
22
|
+
name: "build-a-block",
|
|
23
|
+
description: "Scaffold a block",
|
|
24
|
+
arguments: [
|
|
25
|
+
{ name: "blockName", required: true, description: "The block name" },
|
|
26
|
+
{ name: "tier", required: false },
|
|
27
|
+
],
|
|
28
|
+
body: "Create {{blockName}} in {{tier}}.",
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
const LIST: PromptListData = {
|
|
32
|
+
prompts: [
|
|
33
|
+
{ name: "build-a-block", description: "Scaffold a block", arguments: [] },
|
|
34
|
+
],
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
describe("promptLookupFormatters", () => {
|
|
38
|
+
it("underlines the plain name with an ═ rule (B7)", () => {
|
|
39
|
+
const lines = promptLookupFormatters.plain(LOOKUP).split("\n");
|
|
40
|
+
expect(lines.at(0)).toBe("build-a-block");
|
|
41
|
+
expect(lines.at(1)).toBe("═".repeat(24));
|
|
42
|
+
expect(lines.at(2)).toBe("");
|
|
43
|
+
expect(lines).toContain(" args: blockName (required), tier");
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
it("heads the llm entity at H2 with H3 sub-sections (B2)", () => {
|
|
47
|
+
const lines = promptLookupFormatters.llm(LOOKUP).split("\n");
|
|
48
|
+
expect(lines.at(0)).toBe("## build-a-block");
|
|
49
|
+
expect(lines).toContain("### Arguments");
|
|
50
|
+
expect(lines).toContain("### Template");
|
|
51
|
+
// The old H1 title / H2 sub-sections are gone.
|
|
52
|
+
expect(lines).not.toContain("# build-a-block");
|
|
53
|
+
expect(lines).not.toContain("## Arguments");
|
|
54
|
+
expect(lines).not.toContain("## Template");
|
|
55
|
+
});
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
describe("promptListFormatters", () => {
|
|
59
|
+
it("heads the llm list at H2, never H1 (B2)", () => {
|
|
60
|
+
const out = promptListFormatters.llm(LIST);
|
|
61
|
+
expect(out.startsWith("## Prompts")).toBe(true);
|
|
62
|
+
expect(out).not.toMatch(/^# Prompts/m);
|
|
63
|
+
});
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
describe("promptLookupFormatters — shared style seam (B7)", () => {
|
|
67
|
+
const prevLevel = chalk.level;
|
|
68
|
+
afterEach(() => {
|
|
69
|
+
chalk.level = prevLevel;
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it("styles the plain title on a color-capable TTY", () => {
|
|
73
|
+
chalk.level = 1;
|
|
74
|
+
withStdoutTty(true, () => {
|
|
75
|
+
// The title/rule/label now route through the same style seam block and
|
|
76
|
+
// skill lookups use — previously a prompt title stayed unstyled on a TTY.
|
|
77
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: asserting the ESC byte is the point
|
|
78
|
+
expect(promptLookupFormatters.plain(LOOKUP)).toMatch(/\x1b\[/);
|
|
79
|
+
});
|
|
80
|
+
});
|
|
81
|
+
|
|
82
|
+
it("keeps piped plain output byte-stable off a TTY", () => {
|
|
83
|
+
chalk.level = 3;
|
|
84
|
+
withStdoutTty(undefined, () => {
|
|
85
|
+
const out = promptLookupFormatters.plain(LOOKUP);
|
|
86
|
+
// biome-ignore lint/suspicious/noControlCharactersInRegex: asserting NO ESC byte survives
|
|
87
|
+
expect(out).not.toMatch(/\x1b\[/);
|
|
88
|
+
const lines = out.split("\n");
|
|
89
|
+
expect(lines.at(0)).toBe("build-a-block");
|
|
90
|
+
expect(lines.at(1)).toBe("═".repeat(24));
|
|
91
|
+
expect(lines).toContain(" args: blockName (required), tier");
|
|
92
|
+
});
|
|
93
|
+
});
|
|
94
|
+
});
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
* Formatters for the `prompt` content noun — plain, llm, json.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import { defaultStyle } from "../../kernel/render/style.js";
|
|
5
6
|
import type { Formatters } from "../../kernel/spec/types.js";
|
|
6
7
|
import type {
|
|
7
8
|
PromptArgument,
|
|
@@ -29,7 +30,8 @@ export const promptListFormatters: Formatters<PromptListData> = {
|
|
|
29
30
|
},
|
|
30
31
|
llm(data) {
|
|
31
32
|
if (data.prompts.length === 0) return "_No prompts in the store._";
|
|
32
|
-
|
|
33
|
+
// H2 for a content read — consistent with the shared list renderer (B2).
|
|
34
|
+
const lines = ["## Prompts"];
|
|
33
35
|
for (const prompt of data.prompts) {
|
|
34
36
|
lines.push(
|
|
35
37
|
`- \`${prompt.name}\` — ${prompt.description ?? ""}`.trimEnd(),
|
|
@@ -45,23 +47,38 @@ export const promptListFormatters: Formatters<PromptListData> = {
|
|
|
45
47
|
|
|
46
48
|
export const promptLookupFormatters: Formatters<PromptLookupData> = {
|
|
47
49
|
plain(data) {
|
|
48
|
-
|
|
50
|
+
// Full delegation to `renderLookupPlain` is infeasible: the description line
|
|
51
|
+
// carries no field label and the template body is appended raw, whereas the
|
|
52
|
+
// shared renderer only emits `label: value` fields + indented sections. So
|
|
53
|
+
// route the TITLE + rule + field label through the SAME style seam it uses
|
|
54
|
+
// (bold title, dim rule, cyan label) — closing the TTY drift where a prompt
|
|
55
|
+
// title stayed unstyled — and keep the bespoke body inline. Off a TTY the
|
|
56
|
+
// styler is inert, so piped output stays byte-stable (B7).
|
|
57
|
+
const style = defaultStyle();
|
|
58
|
+
const rule = "═".repeat(Math.max(data.name.length, 24));
|
|
59
|
+
const lines = [style.bold(data.name), style.dim(rule), ""];
|
|
49
60
|
if (data.description) lines.push(` ${data.description}`);
|
|
50
|
-
lines.push(
|
|
61
|
+
lines.push(
|
|
62
|
+
` ${style.cyan("args")}: ${argTokens(data.arguments)}`,
|
|
63
|
+
"",
|
|
64
|
+
data.body,
|
|
65
|
+
);
|
|
51
66
|
return lines.join("\n");
|
|
52
67
|
},
|
|
53
68
|
llm(data) {
|
|
54
|
-
|
|
69
|
+
// H2 entity title with H3 sub-sections — the hierarchy the shared
|
|
70
|
+
// `renderLookupLlm` uses (`## title` / `### section`) (B2).
|
|
71
|
+
const lines = [`## ${data.name}`];
|
|
55
72
|
if (data.description) lines.push(data.description);
|
|
56
73
|
if (data.arguments.length > 0) {
|
|
57
|
-
lines.push("", "
|
|
74
|
+
lines.push("", "### Arguments");
|
|
58
75
|
for (const arg of data.arguments) {
|
|
59
76
|
lines.push(
|
|
60
77
|
`- \`${arg.name}\`${arg.required ? " (required)" : ""}${arg.description ? ` — ${arg.description}` : ""}`,
|
|
61
78
|
);
|
|
62
79
|
}
|
|
63
80
|
}
|
|
64
|
-
lines.push("", "
|
|
81
|
+
lines.push("", "### Template", data.body);
|
|
65
82
|
return lines.join("\n");
|
|
66
83
|
},
|
|
67
84
|
json(data) {
|
|
@@ -1,6 +1,10 @@
|
|
|
1
|
+
import { mkdtempSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
1
4
|
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
2
5
|
import { listPromptSummaries } from "../../kernel/project/mcp/prompts/source.js";
|
|
3
6
|
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
7
|
+
import { writeLock } from "../../kernel/runtime/lock.js";
|
|
4
8
|
import type { GlobalFlags } from "../../kernel/spec/types.js";
|
|
5
9
|
import { CANONICAL_TTL } from "../../testing/fixtures/graph/canonical.js";
|
|
6
10
|
import {
|
|
@@ -117,3 +121,60 @@ describe("prompt — storeless native listing (PROTECTED)", () => {
|
|
|
117
121
|
expect(runtime.store.booted).toBe(false);
|
|
118
122
|
});
|
|
119
123
|
});
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* D1/D4 — cold-store convergence. The `prompt_list` tool is store-backed
|
|
127
|
+
* (`needsStore`), so a cold store throws STORE_UNAVAILABLE through its pre-check;
|
|
128
|
+
* native `prompts/list`/`prompts/get` must present the SAME diagnostic rather
|
|
129
|
+
* than silently listing a stale index (D1) or reporting a bogus "not found"
|
|
130
|
+
* (D4). A lock pointing at an evicted pack is the cold "configured but unbuilt"
|
|
131
|
+
* store (the pack is absent from the XDG-isolated cache).
|
|
132
|
+
*/
|
|
133
|
+
describe("prompt — cold-store convergence, tool + native agree (D1/D4)", () => {
|
|
134
|
+
let cold: McpHarness;
|
|
135
|
+
|
|
136
|
+
beforeAll(async () => {
|
|
137
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-prompt-cold-"));
|
|
138
|
+
writeLock(cwd, { version: 1, contentHash: "b".repeat(64), packs: [] });
|
|
139
|
+
cold = await projectMcp(capabilities, cwd);
|
|
140
|
+
});
|
|
141
|
+
afterAll(async () => {
|
|
142
|
+
await cold.cleanup();
|
|
143
|
+
});
|
|
144
|
+
|
|
145
|
+
it("prompt_list (tool) and prompts/list (native) BOTH surface STORE_UNAVAILABLE", async () => {
|
|
146
|
+
const tool = await cold.callTool("prompt_list");
|
|
147
|
+
expect(tool.ok).toBe(false);
|
|
148
|
+
expect((tool.error as { code: string }).code).toBe("STORE_UNAVAILABLE");
|
|
149
|
+
// The native list no longer diverges into a silent [] / stale index listing.
|
|
150
|
+
await expect(cold.listPrompts()).rejects.toThrow(/store unavailable/i);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
it("both surfaces carry the same `sources_update` recovery cold", async () => {
|
|
154
|
+
const tool = await cold.callTool("prompt_list");
|
|
155
|
+
const toolRecovery = (
|
|
156
|
+
tool.error as { recovery?: { mcp?: { tool?: string } } }
|
|
157
|
+
).recovery;
|
|
158
|
+
expect(toolRecovery?.mcp?.tool).toBe("sources_update");
|
|
159
|
+
|
|
160
|
+
// Native carries the recovery in the JSON-RPC error `data` — never dropped.
|
|
161
|
+
const nativeErr = await cold.listPrompts().then(
|
|
162
|
+
() => undefined,
|
|
163
|
+
(error: unknown) => error,
|
|
164
|
+
);
|
|
165
|
+
const data = (
|
|
166
|
+
nativeErr as { data?: { recovery?: { mcp?: { tool?: string } } } }
|
|
167
|
+
).data;
|
|
168
|
+
expect(data?.recovery?.mcp?.tool).toBe("sources_update");
|
|
169
|
+
});
|
|
170
|
+
|
|
171
|
+
it("native prompts/get reports STORE_UNAVAILABLE cold, not a bogus 'not found' (D4)", async () => {
|
|
172
|
+
const error = await cold.getPrompt("build-a-block").then(
|
|
173
|
+
() => undefined,
|
|
174
|
+
(caught: unknown) => caught as Error,
|
|
175
|
+
);
|
|
176
|
+
expect(error?.message).toMatch(/store unavailable/i);
|
|
177
|
+
// The genuine-miss message must NOT mask the cold store.
|
|
178
|
+
expect(error?.message).not.toMatch(/not found/i);
|
|
179
|
+
});
|
|
180
|
+
});
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { readdirSync, readFileSync } from "node:fs";
|
|
2
|
+
import { fileURLToPath } from "node:url";
|
|
3
|
+
import { describe, expect, it } from "vitest";
|
|
4
|
+
import { emitReference } from "../kernel/spec/emitReference.js";
|
|
5
|
+
import { capabilities } from "./index.js";
|
|
6
|
+
|
|
7
|
+
/** Read a committed reference page exactly as a consumer (or lychee) would. */
|
|
8
|
+
function readCommitted(relPath: string): string {
|
|
9
|
+
return readFileSync(
|
|
10
|
+
fileURLToPath(new URL(`../../docs/reference/${relPath}`, import.meta.url)),
|
|
11
|
+
"utf-8",
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
describe("reference docs drift-guard — emitReference == committed (PROTECTED)", () => {
|
|
16
|
+
const emitted = emitReference(capabilities);
|
|
17
|
+
|
|
18
|
+
it("every generated page is byte-identical to the committed file", () => {
|
|
19
|
+
for (const [relPath, expected] of emitted) {
|
|
20
|
+
expect(readCommitted(relPath)).toBe(expected);
|
|
21
|
+
}
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
it("the committed file set matches the generated key set (no orphans, none missing)", () => {
|
|
25
|
+
const committed = readdirSync(
|
|
26
|
+
fileURLToPath(new URL("../../docs/reference/", import.meta.url)),
|
|
27
|
+
)
|
|
28
|
+
.filter((name) => name.endsWith(".md"))
|
|
29
|
+
.sort();
|
|
30
|
+
expect(committed).toEqual([...emitted.keys()].sort());
|
|
31
|
+
});
|
|
32
|
+
});
|
|
@@ -17,7 +17,9 @@
|
|
|
17
17
|
*/
|
|
18
18
|
|
|
19
19
|
import { readPackIndex } from "../../kernel/completion/entitySource.js";
|
|
20
|
+
import { asPragmaError } from "../../kernel/error/fromTaskError.js";
|
|
20
21
|
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
22
|
+
import { mcpErrorFrom } from "../../kernel/project/mcp/mcpError.js";
|
|
21
23
|
import type { PackIndex } from "../../kernel/runtime/graphpack/types.js";
|
|
22
24
|
import { readEntity } from "../../kernel/runtime/readEntity.js";
|
|
23
25
|
import type { McpResourceProvider } from "../../kernel/spec/types.js";
|
|
@@ -88,7 +90,18 @@ export function buildResourceList(
|
|
|
88
90
|
const boxRank = (e: typeof a) => (e.box === "tbox" ? 0 : 1);
|
|
89
91
|
return boxRank(a) - boxRank(b) || a.name.localeCompare(b.name);
|
|
90
92
|
});
|
|
91
|
-
|
|
93
|
+
// Dedup by resource URI: an OWL-punned subject (a class/property IRI ALSO
|
|
94
|
+
// asserted as a domain individual) is indexed as TWO entities — a tbox and an
|
|
95
|
+
// abox facet — that compact to the SAME `pragma:<uri>`. Mapping both would
|
|
96
|
+
// list one URI twice (A8). The sort above puts the tbox facet first, so
|
|
97
|
+
// keeping the first occurrence per URI surfaces the schema facet (its higher
|
|
98
|
+
// priority + instance count) and drops the punned duplicate.
|
|
99
|
+
const seen = new Set<string>();
|
|
100
|
+
const resources: ListedResource[] = [];
|
|
101
|
+
for (const entity of ordered) {
|
|
102
|
+
const uri = `pragma:${entity.prefixed ?? entity.name}`;
|
|
103
|
+
if (seen.has(uri)) continue;
|
|
104
|
+
seen.add(uri);
|
|
92
105
|
const isTbox = entity.box === "tbox";
|
|
93
106
|
// Port of the old shell's `_meta` taxonomy: `pragma/box` + a priority that
|
|
94
107
|
// ranks schema (classes/properties) above individuals, so an agent browsing
|
|
@@ -104,8 +117,8 @@ export function buildResourceList(
|
|
|
104
117
|
if (typeof instanceCount === "number") {
|
|
105
118
|
meta["pragma/instanceCount"] = instanceCount;
|
|
106
119
|
}
|
|
107
|
-
|
|
108
|
-
uri
|
|
120
|
+
resources.push({
|
|
121
|
+
uri,
|
|
109
122
|
name: entity.label || entity.name,
|
|
110
123
|
...(entity.description ? { description: entity.description } : {}),
|
|
111
124
|
mimeType: "application/json" as const,
|
|
@@ -114,8 +127,9 @@ export function buildResourceList(
|
|
|
114
127
|
priority: isTbox ? CLASS_PRIORITY : INDIVIDUAL_PRIORITY,
|
|
115
128
|
},
|
|
116
129
|
_meta: meta,
|
|
117
|
-
};
|
|
118
|
-
}
|
|
130
|
+
});
|
|
131
|
+
}
|
|
132
|
+
return resources;
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
/**
|
|
@@ -180,6 +194,9 @@ export const resourceProvider: McpResourceProvider = {
|
|
|
180
194
|
const { ResourceTemplate } = await import(
|
|
181
195
|
"@modelcontextprotocol/sdk/server/mcp.js"
|
|
182
196
|
);
|
|
197
|
+
const { McpError, ErrorCode } = await import(
|
|
198
|
+
"@modelcontextprotocol/sdk/types.js"
|
|
199
|
+
);
|
|
183
200
|
const template = new ResourceTemplate(URI_TEMPLATE, {
|
|
184
201
|
list: async () => ({
|
|
185
202
|
resources: buildResourceList(readPackIndex(runtime.cwd)),
|
|
@@ -215,15 +232,12 @@ export const resourceProvider: McpResourceProvider = {
|
|
|
215
232
|
],
|
|
216
233
|
};
|
|
217
234
|
} catch (error) {
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
{ uri: url.href, mimeType: "text/plain", text: message },
|
|
225
|
-
],
|
|
226
|
-
};
|
|
235
|
+
// Surface the failure as a JSON-RPC error (the resource-read analogue
|
|
236
|
+
// of a tool result's `isError`), preserving the machine code AND the
|
|
237
|
+
// recovery in `data` — never swallow it into `text/plain` content an
|
|
238
|
+
// agent reads as a successful entity (which dropped the recovery and
|
|
239
|
+
// masked a cold store as though the entity itself were malformed).
|
|
240
|
+
throw mcpErrorFrom(asPragmaError(error), { McpError, ErrorCode });
|
|
227
241
|
}
|
|
228
242
|
},
|
|
229
243
|
);
|