@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
|
@@ -2,6 +2,8 @@
|
|
|
2
2
|
* Formatters + data shape for `tier lookup <name>`.
|
|
3
3
|
*/
|
|
4
4
|
|
|
5
|
+
import type { RenderLookupOptions } from "../../kernel/render/contracts.js";
|
|
6
|
+
import { renderLookupPlain } from "../../kernel/render/renderers.js";
|
|
5
7
|
import type { Formatters } from "../../kernel/spec/types.js";
|
|
6
8
|
|
|
7
9
|
/** The looked-up tier: its IRI, name, and the blocks scoped directly to it. */
|
|
@@ -11,18 +13,41 @@ export interface TierLookupData {
|
|
|
11
13
|
readonly blocks: readonly string[];
|
|
12
14
|
}
|
|
13
15
|
|
|
16
|
+
/**
|
|
17
|
+
* The shared plain-lookup layout for a tier: a `name (uri)` title over an ═ rule,
|
|
18
|
+
* then the directly-scoped blocks as one inline field. Delegating to
|
|
19
|
+
* {@link renderLookupPlain} single-sources the frame AND the TTY styling (bold
|
|
20
|
+
* title, dim rule, cyan label) that block/skill lookups already earn — so a tier
|
|
21
|
+
* title no longer stays unstyled on a terminal while theirs are bold (B7).
|
|
22
|
+
*/
|
|
23
|
+
const tierPlainOptions: RenderLookupOptions<TierLookupData> = {
|
|
24
|
+
title: (tier) => `${tier.name} (${tier.uri})`,
|
|
25
|
+
fields: [
|
|
26
|
+
{
|
|
27
|
+
label: "blocks",
|
|
28
|
+
// Pre-joined so the empty case still renders (the shared field renderer
|
|
29
|
+
// skips an empty array). Blocks are `ds:name` display strings, never IRIs,
|
|
30
|
+
// so the shared renderer's URI compaction is a no-op here — byte-stable.
|
|
31
|
+
value: (tier) =>
|
|
32
|
+
tier.blocks.length > 0
|
|
33
|
+
? tier.blocks.join(", ")
|
|
34
|
+
: "(none scoped directly to this tier)",
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
sections: [],
|
|
38
|
+
};
|
|
39
|
+
|
|
14
40
|
export const tierLookupFormatters: Formatters<TierLookupData> = {
|
|
15
41
|
plain(data) {
|
|
16
|
-
|
|
17
|
-
lines.push(
|
|
18
|
-
data.blocks.length > 0
|
|
19
|
-
? ` blocks: ${data.blocks.join(", ")}`
|
|
20
|
-
: " blocks: (none scoped directly to this tier)",
|
|
21
|
-
);
|
|
22
|
-
return lines.join("\n");
|
|
42
|
+
return renderLookupPlain(data, tierPlainOptions);
|
|
23
43
|
},
|
|
24
44
|
llm(data) {
|
|
25
|
-
|
|
45
|
+
// A byte-frozen agent contract that DIVERGES from the generic
|
|
46
|
+
// `renderLookupLlm`: an H2 `name` title (not `name (uri)`) with the IRI and
|
|
47
|
+
// blocks backtick-wrapped (`\`cs:…\``) — a shape the shared renderer does not
|
|
48
|
+
// emit. The llm path is never styled, so it has no TTY drift to single-source;
|
|
49
|
+
// kept inline so the frozen bytes hold (B2).
|
|
50
|
+
const lines = [`## ${data.name}`, `- IRI: \`${data.uri}\``];
|
|
26
51
|
if (data.blocks.length > 0) {
|
|
27
52
|
lines.push(`- Blocks: ${data.blocks.map((b) => `\`${b}\``).join(", ")}`);
|
|
28
53
|
}
|
|
@@ -39,6 +39,21 @@ describe("tier_lookup — bespoke single-name lookup (PROTECTED)", () => {
|
|
|
39
39
|
const error = result.error as { code: string };
|
|
40
40
|
expect(error.code).toBe("ENTITY_NOT_FOUND");
|
|
41
41
|
});
|
|
42
|
+
|
|
43
|
+
it("suggests a near-miss tier name from the full list (did-you-mean parity)", async () => {
|
|
44
|
+
// B6: tier lookup was the only entity lookup that omitted suggestions,
|
|
45
|
+
// though — like the others — it can read the full tier list on a miss.
|
|
46
|
+
let caught: unknown;
|
|
47
|
+
try {
|
|
48
|
+
await runTierLookup(fixture.runtime, "apps/lxdd");
|
|
49
|
+
} catch (error) {
|
|
50
|
+
caught = error;
|
|
51
|
+
}
|
|
52
|
+
expect(caught).toMatchObject({ code: "ENTITY_NOT_FOUND" });
|
|
53
|
+
expect((caught as { suggestions: string[] }).suggestions).toContain(
|
|
54
|
+
"apps/lxd",
|
|
55
|
+
);
|
|
56
|
+
});
|
|
42
57
|
});
|
|
43
58
|
|
|
44
59
|
describe("tier_lookup — cold/unseeded store (ROOT A)", () => {
|
|
@@ -9,11 +9,34 @@
|
|
|
9
9
|
|
|
10
10
|
import { PragmaError } from "../../kernel/error/PragmaError.js";
|
|
11
11
|
import { runSelect } from "../../kernel/packs/sparql/runSelect.js";
|
|
12
|
+
import { suggestNames } from "../../kernel/project/cli/suggestNames.js";
|
|
12
13
|
import { compactUri } from "../../kernel/render/compactUri.js";
|
|
13
14
|
import { DEFAULT_PREFIX_MAP } from "../../kernel/render/prefixes.js";
|
|
14
15
|
import type { PragmaRuntime } from "../../kernel/runtime/types.js";
|
|
15
16
|
import type { TierLookupData } from "./lookup.render.js";
|
|
16
17
|
|
|
18
|
+
/**
|
|
19
|
+
* Read every tier name in the store (its `ds:name`), ordered.
|
|
20
|
+
*
|
|
21
|
+
* The bespoke lookup already holds the machinery to reach the store, so a
|
|
22
|
+
* name-not-found can rank "did you mean?" candidates the same way every other
|
|
23
|
+
* entity lookup does — the tier list is a single cheap SELECT away.
|
|
24
|
+
*
|
|
25
|
+
* @param rt - The runtime (its store is booted by the projector for needsStore).
|
|
26
|
+
* @returns The tier names, ordered.
|
|
27
|
+
* @note Impure — queries the store through `runSelect`.
|
|
28
|
+
*/
|
|
29
|
+
async function selectTierNames(rt: PragmaRuntime): Promise<string[]> {
|
|
30
|
+
const rows = await runSelect(
|
|
31
|
+
rt,
|
|
32
|
+
"SELECT ?name WHERE { ?tier a ds:Tier ; ds:name ?name } ORDER BY ?name",
|
|
33
|
+
"tier",
|
|
34
|
+
);
|
|
35
|
+
return rows
|
|
36
|
+
.map((row) => row.name as string | undefined)
|
|
37
|
+
.filter((name): name is string => Boolean(name));
|
|
38
|
+
}
|
|
39
|
+
|
|
17
40
|
/**
|
|
18
41
|
* Look up one tier by name.
|
|
19
42
|
*
|
|
@@ -43,7 +66,12 @@ export async function runTierLookup(
|
|
|
43
66
|
const rows = await runSelect(rt, query, "tier");
|
|
44
67
|
const uri = rows[0]?.uri;
|
|
45
68
|
if (!uri) {
|
|
69
|
+
// Rank "did you mean?" candidates from the full tier list — the only entity
|
|
70
|
+
// lookup that used to omit suggestions, though (like the others) it has the
|
|
71
|
+
// names in hand. The suggestion query runs only on this cold, not-found path.
|
|
72
|
+
const suggestions = suggestNames(name, await selectTierNames(rt));
|
|
46
73
|
throw PragmaError.notFound("tier", name, {
|
|
74
|
+
suggestions,
|
|
47
75
|
recovery: {
|
|
48
76
|
message: "List available tiers.",
|
|
49
77
|
cli: "pragma tier list",
|
package/src/constants.test.ts
CHANGED
|
@@ -25,7 +25,7 @@ describe("constants", () => {
|
|
|
25
25
|
});
|
|
26
26
|
|
|
27
27
|
it("exposes the stable output formats and detail levels", () => {
|
|
28
|
-
expect(OUTPUT_FORMATS).toEqual(["plain", "json"]);
|
|
28
|
+
expect(OUTPUT_FORMATS).toEqual(["plain", "llm", "json"]);
|
|
29
29
|
expect(DETAIL_LEVELS).toEqual(["summary", "standard", "detailed"]);
|
|
30
30
|
expect(DETAIL_LEVELS).toContain(DEFAULT_DETAIL_LEVEL);
|
|
31
31
|
expect(PROGRAM_DESCRIPTION.length).toBeGreaterThan(0);
|
package/src/constants.ts
CHANGED
|
@@ -31,8 +31,8 @@ const VERSION: string = pkg.version;
|
|
|
31
31
|
*/
|
|
32
32
|
const RECOVERY_CLI_PREFIX = "pragma ";
|
|
33
33
|
|
|
34
|
-
/** The
|
|
35
|
-
const OUTPUT_FORMATS = ["plain", "json"] as const;
|
|
34
|
+
/** The output formats the renderer selects between (`llm` = condensed Markdown). */
|
|
35
|
+
const OUTPUT_FORMATS = ["plain", "llm", "json"] as const;
|
|
36
36
|
|
|
37
37
|
/** A selected output format. `--format text` is normalised to `plain`. */
|
|
38
38
|
type OutputFormat = (typeof OUTPUT_FORMATS)[number];
|
|
@@ -58,21 +58,21 @@ _pragma() {
|
|
|
58
58
|
block/list/--tags) return ;;
|
|
59
59
|
esac
|
|
60
60
|
case "$prev" in
|
|
61
|
-
--format) COMPREPLY=($(compgen -W "plain json" -- "$cur")); return ;;
|
|
61
|
+
--format) COMPREPLY=($(compgen -W "plain llm json" -- "$cur")); return ;;
|
|
62
62
|
--detail) COMPREPLY=($(compgen -W "summary standard detailed" -- "$cur")); return ;;
|
|
63
63
|
esac
|
|
64
64
|
|
|
65
65
|
# flag names
|
|
66
66
|
if [[ "$cur" == -* ]]; then
|
|
67
67
|
case "$noun/$verb" in
|
|
68
|
-
block/get) COMPREPLY=($(compgen -W "--view --out --with-meta --note --
|
|
69
|
-
block/list) COMPREPLY=($(compgen -W "--tags --
|
|
70
|
-
block/remove) COMPREPLY=($(compgen -W "--dry-run --undo --yes --
|
|
68
|
+
block/get) COMPREPLY=($(compgen -W "--view --out --with-meta --note --format --verbose --detail --help" -- "$cur")); return ;;
|
|
69
|
+
block/list) COMPREPLY=($(compgen -W "--tags --format --verbose --detail --help" -- "$cur")); return ;;
|
|
70
|
+
block/remove) COMPREPLY=($(compgen -W "--dry-run --undo --yes --format --verbose --detail --help" -- "$cur")); return ;;
|
|
71
71
|
esac
|
|
72
72
|
if [ -z "$noun" ]; then
|
|
73
|
-
COMPREPLY=($(compgen -W "--
|
|
73
|
+
COMPREPLY=($(compgen -W "--format --verbose --detail --help --version" -- "$cur"))
|
|
74
74
|
else
|
|
75
|
-
COMPREPLY=($(compgen -W "--
|
|
75
|
+
COMPREPLY=($(compgen -W "--format --verbose --detail --help" -- "$cur"))
|
|
76
76
|
fi
|
|
77
77
|
return
|
|
78
78
|
fi
|
|
@@ -172,8 +172,7 @@ complete -c pragma -f
|
|
|
172
172
|
complete -c pragma -n "__pragma_at ''" -a "block mcp standard status"
|
|
173
173
|
|
|
174
174
|
# global flags
|
|
175
|
-
complete -c pragma -l llm
|
|
176
|
-
complete -c pragma -l format -x -a "plain json"
|
|
175
|
+
complete -c pragma -l format -x -a "plain llm json"
|
|
177
176
|
complete -c pragma -l verbose
|
|
178
177
|
complete -c pragma -l detail -x -a "summary standard detailed"
|
|
179
178
|
complete -c pragma -l help
|
|
@@ -268,21 +267,21 @@ _pragma() {
|
|
|
268
267
|
block/list/--tags) return 0 ;;
|
|
269
268
|
esac
|
|
270
269
|
case "$prev" in
|
|
271
|
-
--format) compadd -- plain json; return 0 ;;
|
|
270
|
+
--format) compadd -- plain llm json; return 0 ;;
|
|
272
271
|
--detail) compadd -- summary standard detailed; return 0 ;;
|
|
273
272
|
esac
|
|
274
273
|
|
|
275
274
|
# flag names
|
|
276
275
|
if [[ "$cur" == -* ]]; then
|
|
277
276
|
case "$noun/$verb" in
|
|
278
|
-
block/get) compadd -- --view --out --with-meta --note --
|
|
279
|
-
block/list) compadd -- --tags --
|
|
280
|
-
block/remove) compadd -- --dry-run --undo --yes --
|
|
277
|
+
block/get) compadd -- --view --out --with-meta --note --format --verbose --detail --help; return 0 ;;
|
|
278
|
+
block/list) compadd -- --tags --format --verbose --detail --help; return 0 ;;
|
|
279
|
+
block/remove) compadd -- --dry-run --undo --yes --format --verbose --detail --help; return 0 ;;
|
|
281
280
|
esac
|
|
282
281
|
if [[ -z "$noun" ]]; then
|
|
283
|
-
compadd -- --
|
|
282
|
+
compadd -- --format --verbose --detail --help --version
|
|
284
283
|
else
|
|
285
|
-
compadd -- --
|
|
284
|
+
compadd -- --format --verbose --detail --help
|
|
286
285
|
fi
|
|
287
286
|
return 0
|
|
288
287
|
fi
|
|
@@ -51,7 +51,7 @@ describe.skipIf(!bashOk)("generated bash — --flag=value wordbreak (M1)", () =>
|
|
|
51
51
|
it("routes --format=<TAB> to the format values (not nouns/positionals)", () => {
|
|
52
52
|
// Default COMP_WORDBREAKS split: `--format` `=` `` (empty current word).
|
|
53
53
|
const reply = driveBash(["pragma", "--format", "=", ""], 3);
|
|
54
|
-
expect(reply.sort()).toEqual(["json", "plain"]);
|
|
54
|
+
expect(reply.sort()).toEqual(["json", "llm", "plain"]);
|
|
55
55
|
});
|
|
56
56
|
|
|
57
57
|
it("filters the value by the partial typed after =", () => {
|
|
@@ -63,11 +63,11 @@ describe.skipIf(!bashOk)("generated bash — --flag=value wordbreak (M1)", () =>
|
|
|
63
63
|
// After a noun/verb the value must still route as a flag value, never as
|
|
64
64
|
// block's positional (which would offer entity/enum candidates instead).
|
|
65
65
|
const reply = driveBash(["pragma", "block", "get", "--format", "=", ""], 5);
|
|
66
|
-
expect(reply.sort()).toEqual(["json", "plain"]);
|
|
66
|
+
expect(reply.sort()).toEqual(["json", "llm", "plain"]);
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
it("still completes the space form --format <TAB> (unbroken)", () => {
|
|
70
70
|
const reply = driveBash(["pragma", "--format", ""], 2);
|
|
71
|
-
expect(reply.sort()).toEqual(["json", "plain"]);
|
|
71
|
+
expect(reply.sort()).toEqual(["json", "llm", "plain"]);
|
|
72
72
|
});
|
|
73
73
|
});
|
|
@@ -19,16 +19,14 @@ describe("runComplete — the __complete pipeline", () => {
|
|
|
19
19
|
"config",
|
|
20
20
|
]);
|
|
21
21
|
// The live `config` sub-verbs, prefix-filtered and sorted by the resolver.
|
|
22
|
+
// AV-228 B3 retired the per-field tier/channel/detail verbs, leaving set + show.
|
|
22
23
|
await expect(runComplete(["config", ""], capabilities)).resolves.toEqual([
|
|
23
|
-
"channel",
|
|
24
|
-
"detail",
|
|
25
24
|
"set",
|
|
26
25
|
"show",
|
|
27
|
-
"tier",
|
|
28
26
|
]);
|
|
29
27
|
await expect(runComplete(["mc"], capabilities)).resolves.toEqual(["mcp"]);
|
|
30
|
-
await expect(runComplete(["--
|
|
31
|
-
"--
|
|
28
|
+
await expect(runComplete(["--d"], capabilities)).resolves.toEqual([
|
|
29
|
+
"--detail",
|
|
32
30
|
]);
|
|
33
31
|
});
|
|
34
32
|
|
|
@@ -98,18 +98,26 @@ export function readPackIndex(cwd: string): PackIndex | undefined {
|
|
|
98
98
|
}
|
|
99
99
|
|
|
100
100
|
/**
|
|
101
|
-
*
|
|
101
|
+
* Count a pack index's DISTINCT abox subjects — the "total entities" figure
|
|
102
102
|
* `info` and `doctor` report. Works over any {@link PackIndex}, whether read
|
|
103
103
|
* storelessly via {@link readPackIndex} or taken from a booted store session.
|
|
104
104
|
*
|
|
105
|
+
* NOT a sum of `instanceCountByType`: on a real OWL/Protégé export that raw
|
|
106
|
+
* multiset double-counts — every individual is typed as both its domain class
|
|
107
|
+
* AND `owl:NamedIndividual`, and the `owl:Class`/property meta-buckets pile on
|
|
108
|
+
* top — so the total ran ~2× the real entity count (A1). Counting distinct
|
|
109
|
+
* abox subjects (the individuals, each once) is the figure users expect; the
|
|
110
|
+
* tbox schema classes/properties are not "entities" in this count.
|
|
111
|
+
*
|
|
105
112
|
* @param index - A pack index.
|
|
106
|
-
* @returns The
|
|
113
|
+
* @returns The number of distinct abox subjects indexed.
|
|
107
114
|
*/
|
|
108
115
|
export function entityTotal(index: PackIndex): number {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
116
|
+
const subjects = new Set<string>();
|
|
117
|
+
for (const entity of index.entities) {
|
|
118
|
+
if (entity.box === "abox") subjects.add(entity.uri ?? entity.name);
|
|
119
|
+
}
|
|
120
|
+
return subjects.size;
|
|
113
121
|
}
|
|
114
122
|
|
|
115
123
|
/** Load the active pack's index: the locked pack, else the embedded fallback. */
|
|
@@ -264,13 +264,13 @@ describe("buildCompletionModel — flags and sources", () => {
|
|
|
264
264
|
const flags = Object.fromEntries(model.globalFlags.map((f) => [f.flag, f]));
|
|
265
265
|
expect(flags["--format"]?.source).toEqual({
|
|
266
266
|
kind: "values",
|
|
267
|
-
values: ["plain", "json"],
|
|
267
|
+
values: ["plain", "llm", "json"],
|
|
268
268
|
});
|
|
269
269
|
expect(flags["--detail"]?.source).toEqual({
|
|
270
270
|
kind: "values",
|
|
271
271
|
values: ["summary", "standard", "detailed"],
|
|
272
272
|
});
|
|
273
|
-
expect(flags["--llm"]
|
|
273
|
+
expect(flags["--llm"]).toBeUndefined();
|
|
274
274
|
expect(flags["--version"]?.rootOnly).toBe(true);
|
|
275
275
|
expect(flags["--help"]?.rootOnly).toBeUndefined();
|
|
276
276
|
});
|
|
@@ -61,7 +61,6 @@ export function assertSafeToken(token: string, where: string): void {
|
|
|
61
61
|
|
|
62
62
|
/** The global flags completion offers, mirroring the surface's globalFlags. */
|
|
63
63
|
const GLOBAL_FLAGS: readonly FlagEntry[] = [
|
|
64
|
-
{ flag: "--llm", takesValue: false, repeatable: false, source: none() },
|
|
65
64
|
{
|
|
66
65
|
flag: "--format",
|
|
67
66
|
takesValue: true,
|
|
@@ -73,7 +73,7 @@ describe("parseWords — nouns and verbs (PROTECTED)", () => {
|
|
|
73
73
|
context: { kind: "noun" },
|
|
74
74
|
partial: "blo",
|
|
75
75
|
});
|
|
76
|
-
expect(parse(["--
|
|
76
|
+
expect(parse(["--verbose", "block", ""]).context).toEqual({
|
|
77
77
|
kind: "verb",
|
|
78
78
|
noun: "block",
|
|
79
79
|
});
|
|
@@ -83,7 +83,6 @@ describe("parseWords — nouns and verbs (PROTECTED)", () => {
|
|
|
83
83
|
describe("parseWords — flag names (PROTECTED)", () => {
|
|
84
84
|
it("offers only globals (with --version) at the root", () => {
|
|
85
85
|
expect(flagsOf(parse(["--"]).context)).toEqual([
|
|
86
|
-
"--llm",
|
|
87
86
|
"--format",
|
|
88
87
|
"--verbose",
|
|
89
88
|
"--detail",
|
|
@@ -105,7 +104,6 @@ describe("parseWords — flag names (PROTECTED)", () => {
|
|
|
105
104
|
"--out",
|
|
106
105
|
"--with-meta",
|
|
107
106
|
"--note",
|
|
108
|
-
"--llm",
|
|
109
107
|
"--format",
|
|
110
108
|
"--verbose",
|
|
111
109
|
"--detail",
|
|
@@ -192,7 +190,7 @@ describe("parseWords — flag values (PROTECTED)", () => {
|
|
|
192
190
|
context: {
|
|
193
191
|
kind: "flag-value",
|
|
194
192
|
flag: "--format",
|
|
195
|
-
source: { kind: "values", values: ["plain", "json"] },
|
|
193
|
+
source: { kind: "values", values: ["plain", "llm", "json"] },
|
|
196
194
|
},
|
|
197
195
|
});
|
|
198
196
|
expect(parse(["block", "get", "--detail", "s"])).toMatchObject({
|
|
@@ -26,10 +26,10 @@ describe("PragmaError factories", () => {
|
|
|
26
26
|
|
|
27
27
|
it("invalidInput enumerates valid options", () => {
|
|
28
28
|
const error = PragmaError.invalidInput("format", "yaml", {
|
|
29
|
-
validOptions: ["plain", "json"],
|
|
29
|
+
validOptions: ["plain", "llm", "json"],
|
|
30
30
|
});
|
|
31
31
|
expect(error.code).toBe("INVALID_INPUT");
|
|
32
|
-
expect(error.validOptions).toEqual(["plain", "json"]);
|
|
32
|
+
expect(error.validOptions).toEqual(["plain", "llm", "json"]);
|
|
33
33
|
});
|
|
34
34
|
|
|
35
35
|
it("unknownVerb names the token", () => {
|
|
@@ -105,14 +105,14 @@ describe("envelope builders", () => {
|
|
|
105
105
|
|
|
106
106
|
it("wraps an error into the failure envelope", () => {
|
|
107
107
|
const error = PragmaError.invalidInput("format", "yaml", {
|
|
108
|
-
validOptions: ["plain", "json"],
|
|
108
|
+
validOptions: ["plain", "llm", "json"],
|
|
109
109
|
});
|
|
110
110
|
expect(errorEnvelope(error)).toEqual({
|
|
111
111
|
ok: false,
|
|
112
112
|
error: {
|
|
113
113
|
code: "INVALID_INPUT",
|
|
114
114
|
message: 'Invalid format "yaml".',
|
|
115
|
-
validOptions: ["plain", "json"],
|
|
115
|
+
validOptions: ["plain", "llm", "json"],
|
|
116
116
|
},
|
|
117
117
|
});
|
|
118
118
|
});
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Render a {@link PragmaError} for each output surface.
|
|
3
3
|
*
|
|
4
4
|
* Plain text targets a human terminal (stderr), condensed Markdown targets an
|
|
5
|
-
* agent reading `--llm`, and JSON emits the `{ ok: false, error }` envelope —
|
|
5
|
+
* agent reading `--format llm`, and JSON emits the `{ ok: false, error }` envelope —
|
|
6
6
|
* the same failure shape the MCP projector returns, so agents see one contract
|
|
7
7
|
* whichever transport they use.
|
|
8
8
|
*/
|
|
@@ -145,7 +145,7 @@ function compileListVerb(shape: PackList, meta: ListVerbMeta): VerbSpec {
|
|
|
145
145
|
},
|
|
146
146
|
]
|
|
147
147
|
: []),
|
|
148
|
-
{ cmd: `pragma ${meta.noun} ${meta.verb} --llm` },
|
|
148
|
+
{ cmd: `pragma ${meta.noun} ${meta.verb} --format llm` },
|
|
149
149
|
],
|
|
150
150
|
capability: READ_CAPABILITY,
|
|
151
151
|
run: (params: Record<string, unknown>, rt: PragmaRuntime) =>
|
|
@@ -61,6 +61,50 @@ describe("buildProgram — flags", () => {
|
|
|
61
61
|
});
|
|
62
62
|
});
|
|
63
63
|
|
|
64
|
+
describe("buildProgram — default-true boolean negation (B9)", () => {
|
|
65
|
+
const negatableVerb: VerbSpec = {
|
|
66
|
+
path: ["thing", "do"],
|
|
67
|
+
summary: "thing do summary",
|
|
68
|
+
params: [
|
|
69
|
+
{ kind: "boolean", name: "ssr", doc: "Include SSR.", default: true },
|
|
70
|
+
{
|
|
71
|
+
kind: "boolean",
|
|
72
|
+
name: "runInstall",
|
|
73
|
+
doc: "Install now.",
|
|
74
|
+
default: false,
|
|
75
|
+
},
|
|
76
|
+
{ kind: "boolean", name: "withHistory", doc: "Seed history." },
|
|
77
|
+
],
|
|
78
|
+
output: {
|
|
79
|
+
formatters: {
|
|
80
|
+
plain: (d) => String(d),
|
|
81
|
+
llm: (d) => String(d),
|
|
82
|
+
json: (d) => JSON.stringify(d),
|
|
83
|
+
},
|
|
84
|
+
},
|
|
85
|
+
capability: { needsStore: false, mutates: false, mcp: { expose: true } },
|
|
86
|
+
run: async () => null,
|
|
87
|
+
};
|
|
88
|
+
const program = projectCli([{ name: "thing", verbs: [negatableVerb] }]);
|
|
89
|
+
const doCmd = program.commands
|
|
90
|
+
.find((c) => c.name() === "thing")
|
|
91
|
+
?.commands.find((c) => c.name() === "do");
|
|
92
|
+
|
|
93
|
+
it("pairs a `--no-` negation onto a default-true boolean so it is disableable", () => {
|
|
94
|
+
const longs = doCmd?.options.map((o) => o.long) ?? [];
|
|
95
|
+
expect(longs).toContain("--ssr");
|
|
96
|
+
expect(longs).toContain("--no-ssr");
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it("leaves default-false / defaultless booleans un-negated (no Commander default-flip)", () => {
|
|
100
|
+
const longs = doCmd?.options.map((o) => o.long) ?? [];
|
|
101
|
+
expect(longs).toContain("--run-install");
|
|
102
|
+
expect(longs).not.toContain("--no-run-install");
|
|
103
|
+
expect(longs).toContain("--with-history");
|
|
104
|
+
expect(longs).not.toContain("--no-with-history");
|
|
105
|
+
});
|
|
106
|
+
});
|
|
107
|
+
|
|
64
108
|
describe("buildProgram — early exits", () => {
|
|
65
109
|
it("throws commander.version for --version, writing the version", async () => {
|
|
66
110
|
const program = projectCli([fixtureModule]);
|
|
@@ -353,7 +397,8 @@ describe("formatRootHelp — grouping", () => {
|
|
|
353
397
|
|
|
354
398
|
it("shows the global flags block", () => {
|
|
355
399
|
expect(help).toContain("Global flags");
|
|
356
|
-
expect(help).toContain("--llm");
|
|
400
|
+
expect(help).not.toContain("--llm");
|
|
401
|
+
expect(help).toContain("--format <plain|llm|json>");
|
|
357
402
|
expect(help).toContain("--detail <level>");
|
|
358
403
|
});
|
|
359
404
|
|
|
@@ -374,12 +419,11 @@ describe("formatRootHelp — grouping", () => {
|
|
|
374
419
|
mcp Start the MCP server over stdio
|
|
375
420
|
|
|
376
421
|
Global flags
|
|
377
|
-
--llm
|
|
378
|
-
--
|
|
379
|
-
--
|
|
380
|
-
--
|
|
381
|
-
--
|
|
382
|
-
--version Show the CLI version
|
|
422
|
+
--format <plain|llm|json> Select output format (llm = condensed Markdown for agents)
|
|
423
|
+
--detail <level> Progressive-disclosure level (summary, standard, detailed)
|
|
424
|
+
--verbose Diagnostic output on stderr
|
|
425
|
+
--help Show help (works on any command)
|
|
426
|
+
--version Show the CLI version
|
|
383
427
|
|
|
384
428
|
Run \`pragma <command> --help\` for details, or \`pragma capabilities\` to get oriented."
|
|
385
429
|
`);
|
|
@@ -70,6 +70,17 @@ function registerParams(command: Command, verb: VerbSpec): void {
|
|
|
70
70
|
} else {
|
|
71
71
|
command.option(spec, param.doc);
|
|
72
72
|
}
|
|
73
|
+
// B9: a default-`true` boolean is otherwise undisableable — the grammar has
|
|
74
|
+
// no negation form, so `create`'s sibling booleans (--ssr/--with-styles/…)
|
|
75
|
+
// could never be turned off. Register a paired `--no-<flag>` so they can:
|
|
76
|
+
// Commander keeps the `true` default and lets `--no-<flag>` set it false
|
|
77
|
+
// (verified: an unset default-false boolean is left alone). This is a CLI
|
|
78
|
+
// PARSE convenience, never a ParamSpec — `emitSurface`/MCP/the covenant are
|
|
79
|
+
// untouched — so the frozen flag list stands and negation stays additive.
|
|
80
|
+
if (param.kind === "boolean" && param.default === true) {
|
|
81
|
+
const flag = kebabCase(param.name);
|
|
82
|
+
command.option(`--no-${flag}`, `Disable --${flag} (on by default).`);
|
|
83
|
+
}
|
|
73
84
|
}
|
|
74
85
|
|
|
75
86
|
if (verb.capability.mutates) {
|
|
@@ -9,10 +9,18 @@ const TTY = { isTty: true, noAutoLlm: false };
|
|
|
9
9
|
const PIPE = { isTty: false, noAutoLlm: false };
|
|
10
10
|
|
|
11
11
|
describe("parseGlobalFlags", () => {
|
|
12
|
-
it("honours an explicit --llm", () => {
|
|
13
|
-
expect(parseGlobalFlags(["--llm"], TTY)).toMatchObject({
|
|
12
|
+
it("honours an explicit --format llm (condensed even on a TTY)", () => {
|
|
13
|
+
expect(parseGlobalFlags(["--format", "llm"], TTY)).toMatchObject({
|
|
14
14
|
llm: true,
|
|
15
15
|
autoLlm: false,
|
|
16
|
+
format: "llm",
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it("forces human output with --format plain down a pipe", () => {
|
|
21
|
+
expect(parseGlobalFlags(["--format", "plain"], PIPE)).toMatchObject({
|
|
22
|
+
llm: false,
|
|
23
|
+
autoLlm: false,
|
|
16
24
|
format: "plain",
|
|
17
25
|
});
|
|
18
26
|
});
|
|
@@ -69,7 +77,7 @@ describe("stripGlobalFlags", () => {
|
|
|
69
77
|
"list",
|
|
70
78
|
]);
|
|
71
79
|
expect(
|
|
72
|
-
stripGlobalFlags(["block", "--
|
|
80
|
+
stripGlobalFlags(["block", "--verbose", "list", "--detail", "detailed"]),
|
|
73
81
|
).toEqual(["block", "list"]);
|
|
74
82
|
expect(stripGlobalFlags(["--format=json", "config", "show"])).toEqual([
|
|
75
83
|
"config",
|
|
@@ -90,7 +98,7 @@ describe("readRawFormat", () => {
|
|
|
90
98
|
|
|
91
99
|
it("reports a valueless --format as empty string", () => {
|
|
92
100
|
expect(readRawFormat(["--format"])).toBe("");
|
|
93
|
-
expect(readRawFormat(["--format", "--
|
|
101
|
+
expect(readRawFormat(["--format", "--verbose"])).toBe("");
|
|
94
102
|
});
|
|
95
103
|
|
|
96
104
|
it("returns undefined when --format is absent", () => {
|
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Global-flag pre-parsing, ahead of Commander.
|
|
3
3
|
*
|
|
4
|
-
* `--
|
|
5
|
-
*
|
|
4
|
+
* `--format`, `--verbose`, and `--detail` may appear anywhere on the line, so
|
|
5
|
+
* they are scanned and stripped before Commander sees argv — otherwise
|
|
6
6
|
* `enablePositionalOptions()` scoping would reject a flag placed after a verb.
|
|
7
7
|
* Ported from the v1 `parseGlobalFlags`, with two v2 changes: a new `--detail`
|
|
8
8
|
* flag and the `--format text` value renamed to `plain` (the kernel's
|
|
9
|
-
* {@link OutputFormat}).
|
|
10
|
-
*
|
|
9
|
+
* {@link OutputFormat}). The dedicated `--llm` flag was folded into
|
|
10
|
+
* `--format llm`, leaving auto-detection as the sole implicit trigger. Both the
|
|
11
|
+
* space (`--format json`) and equals (`--format=json`) forms are recognized.
|
|
11
12
|
*/
|
|
12
13
|
|
|
13
14
|
import { DETAIL_LEVELS, type DetailLevel } from "../../../constants.js";
|
|
@@ -86,10 +87,11 @@ function readDetail(argv: readonly string[]): DetailLevel | undefined {
|
|
|
86
87
|
/**
|
|
87
88
|
* Extract the global flags from raw argv (the user's arguments, argv[2:]).
|
|
88
89
|
*
|
|
89
|
-
* Auto-LLM: with no `--
|
|
90
|
+
* Auto-LLM: with no explicit `--format` and a non-interactive stdout (piped or
|
|
90
91
|
* redirected — the shape an agent captures), `llm` defaults to true so agents
|
|
91
|
-
* get condensed Markdown without a flag.
|
|
92
|
-
* `
|
|
92
|
+
* get condensed Markdown without a flag. Any explicit `--format` overrides it —
|
|
93
|
+
* `--format plain` forces human output down a pipe, `--format llm` forces the
|
|
94
|
+
* condensed form even on a TTY — as does `PRAGMA_NO_AUTO_LLM`.
|
|
93
95
|
*
|
|
94
96
|
* @param argv - The user's arguments (no `node`/script prefix).
|
|
95
97
|
* @param env - Output environment probe (defaults to the real process).
|
|
@@ -100,15 +102,14 @@ export function parseGlobalFlags(
|
|
|
100
102
|
argv: readonly string[],
|
|
101
103
|
env: OutputEnvironment = readOutputEnvironment(),
|
|
102
104
|
): GlobalFlags {
|
|
103
|
-
const explicitLlm = argv.includes("--llm");
|
|
104
105
|
const rawFormat = readFlagValue(argv, "--format");
|
|
105
|
-
const format =
|
|
106
|
+
const format =
|
|
107
|
+
rawFormat === "json" ? "json" : rawFormat === "llm" ? "llm" : "plain";
|
|
106
108
|
const formatRequested = rawFormat !== undefined;
|
|
107
|
-
const autoLlm =
|
|
108
|
-
!explicitLlm && !formatRequested && !env.isTty && !env.noAutoLlm;
|
|
109
|
+
const autoLlm = !formatRequested && !env.isTty && !env.noAutoLlm;
|
|
109
110
|
const detail = readDetail(argv);
|
|
110
111
|
return {
|
|
111
|
-
llm:
|
|
112
|
+
llm: format === "llm" || autoLlm,
|
|
112
113
|
autoLlm,
|
|
113
114
|
format,
|
|
114
115
|
verbose: argv.includes("--verbose"),
|
|
@@ -130,10 +131,9 @@ export function stripGlobalFlags(argv: readonly string[]): string[] {
|
|
|
130
131
|
const result: string[] = [];
|
|
131
132
|
for (let i = 0; i < argv.length; i++) {
|
|
132
133
|
const arg = argv[i];
|
|
133
|
-
if (arg === "--
|
|
134
|
+
if (arg === "--verbose") continue;
|
|
134
135
|
if (
|
|
135
136
|
arg?.startsWith("--format=") ||
|
|
136
|
-
arg?.startsWith("--llm=") ||
|
|
137
137
|
arg?.startsWith("--verbose=") ||
|
|
138
138
|
arg?.startsWith("--detail=")
|
|
139
139
|
) {
|
|
@@ -195,8 +195,10 @@ export function formatRootHelp(
|
|
|
195
195
|
// never these strings.
|
|
196
196
|
lines.push(helpHeading("Global flags"));
|
|
197
197
|
const flags: [string, string][] = [
|
|
198
|
-
[
|
|
199
|
-
|
|
198
|
+
[
|
|
199
|
+
"--format <plain|llm|json>",
|
|
200
|
+
"Select output format (llm = condensed Markdown for agents)",
|
|
201
|
+
],
|
|
200
202
|
[
|
|
201
203
|
"--detail <level>",
|
|
202
204
|
"Progressive-disclosure level (summary, standard, detailed)",
|
|
@@ -38,7 +38,7 @@ export function buildInstructions(
|
|
|
38
38
|
|
|
39
39
|
return [
|
|
40
40
|
"pragma is a CLI and MCP server over a design-system knowledge graph.",
|
|
41
|
-
`${CONVENTIONS.system} ${CONVENTIONS.model} ${CONVENTIONS.querying}`,
|
|
41
|
+
`${CONVENTIONS.system} ${CONVENTIONS.model} ${CONVENTIONS.querying} ${CONVENTIONS.mutations}`,
|
|
42
42
|
"",
|
|
43
43
|
"Discovery sequence:",
|
|
44
44
|
steps,
|