@canonical/pragma-cli 0.27.1-experimental.0 → 0.29.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/dist/pragma +0 -0
- package/package.json +16 -17
- package/src/config/readConfig.ts +38 -2
- package/src/config/types.ts +12 -1
- package/src/config/writeConfig.ts +12 -0
- package/src/constants.ts +12 -1
- package/src/domains/block/commands/index.ts +2 -1
- package/src/domains/block/commands/sample.ts +55 -0
- package/src/domains/block/formatters/index.ts +2 -1
- package/src/domains/block/formatters/sample.ts +21 -0
- package/src/domains/block/index.ts +3 -3
- package/src/domains/block/mcp/specs.ts +48 -0
- package/src/domains/block/operations/index.ts +2 -1
- package/src/domains/block/operations/lookup.ts +9 -0
- package/src/domains/block/operations/sample.ts +41 -0
- package/src/domains/block/orchestration/blockEmptyError.test.ts +26 -0
- package/src/domains/block/orchestration/resolveBlockLookup.ts +10 -3
- package/src/domains/config/commands/framework.test.ts +88 -0
- package/src/domains/config/commands/framework.ts +85 -0
- package/src/domains/config/commands/index.ts +2 -0
- package/src/domains/config/commands/trace.test.ts +148 -0
- package/src/domains/config/commands/trace.ts +80 -0
- package/src/domains/config/formatters/framework.ts +34 -0
- package/src/domains/config/formatters/index.ts +2 -0
- package/src/domains/config/formatters/trace.ts +28 -0
- package/src/domains/config/index.ts +15 -2
- package/src/domains/config/operations/index.ts +1 -0
- package/src/domains/config/operations/validateFramework.ts +24 -0
- package/src/domains/config/tasks/index.ts +2 -0
- package/src/domains/config/tasks/setChannel.ts +7 -1
- package/src/domains/config/tasks/setFramework.ts +40 -0
- package/src/domains/config/tasks/setTier.ts +7 -1
- package/src/domains/config/tasks/setTrace.ts +38 -0
- package/src/domains/doctor/operations/checks/checkKeStore.ts +5 -5
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +35 -41
- package/src/domains/doctor/operations/checks.test.ts +4 -1
- package/src/domains/doctor/operations/runChecks.test.ts +72 -0
- package/src/domains/doctor/operations/runChecks.ts +48 -12
- package/src/domains/graphql/commands/build.test.ts +194 -0
- package/src/domains/graphql/commands/build.ts +125 -0
- package/src/domains/graphql/commands/check.test.ts +139 -0
- package/src/domains/graphql/commands/check.ts +87 -0
- package/src/domains/graphql/commands/index.ts +4 -0
- package/src/domains/graphql/formatters/index.ts +3 -0
- package/src/domains/graphql/formatters/report.ts +78 -0
- package/src/domains/graphql/helpers/index.ts +4 -0
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +44 -0
- package/src/domains/graphql/helpers/parsePrefixes.ts +38 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +56 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +45 -0
- package/src/domains/graphql/index.ts +21 -0
- package/src/domains/graphql/operations/compileSchema.test.ts +119 -0
- package/src/domains/graphql/operations/compileSchema.ts +109 -0
- package/src/domains/graphql/operations/index.ts +7 -0
- package/src/domains/graphql/types.ts +29 -0
- package/src/domains/info/operations/collectInfo.test.ts +2 -2
- package/src/domains/info/operations/collectInfo.ts +4 -4
- package/src/domains/llm/capabilities.test.ts +91 -0
- package/src/domains/llm/commands/capabilities.ts +28 -0
- package/src/domains/llm/commands/index.ts +1 -0
- package/src/domains/llm/data/commandReference.ts +90 -18
- package/src/domains/llm/data/decisionTrees.ts +15 -13
- package/src/domains/llm/data/index.ts +2 -0
- package/src/domains/llm/data/toolCatalog.ts +184 -0
- package/src/domains/llm/formatters/capabilities.ts +50 -0
- package/src/domains/llm/formatters/index.ts +1 -0
- package/src/domains/llm/index.ts +3 -2
- package/src/domains/llm/mcp/index.ts +1 -1
- package/src/domains/llm/mcp/specs.ts +59 -52
- package/src/domains/llm/types.ts +40 -0
- package/src/domains/modifier/commands/index.ts +2 -1
- package/src/domains/modifier/commands/sample.ts +53 -0
- package/src/domains/modifier/formatters/index.ts +2 -1
- package/src/domains/modifier/formatters/sample.ts +15 -0
- package/src/domains/modifier/index.ts +7 -3
- package/src/domains/modifier/mcp/specs.ts +47 -0
- package/src/domains/modifier/operations/index.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +9 -0
- package/src/domains/modifier/operations/sample.ts +32 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.test.ts +26 -0
- package/src/domains/modifier/orchestration/modifierEmptyError.ts +7 -2
- package/src/domains/modifier/orchestration/resolveModifierLookup.ts +14 -2
- package/src/domains/refs/operations/index.ts +1 -1
- package/src/domains/refs/operations/paths.ts +29 -1
- package/src/domains/refs/operations/updateRefs.test.ts +6 -3
- package/src/domains/refs/operations/updateRefs.ts +3 -2
- package/src/domains/shared/bootStore.ts +88 -49
- package/src/domains/shared/contracts.ts +1 -0
- package/src/domains/shared/emptyErrorRecovery.test.ts +109 -0
- package/src/domains/shared/loaders/bundledLoader.ts +108 -0
- package/src/domains/shared/loaders/gitLoader.ts +31 -0
- package/src/domains/shared/loaders/index.ts +6 -0
- package/src/domains/shared/loaders/localLoader.ts +73 -0
- package/src/domains/shared/loaders/readPackageDir.ts +112 -0
- package/src/domains/shared/lookupMany.ts +9 -1
- package/src/domains/shared/packages.ts +21 -120
- package/src/domains/shared/pickRandom.ts +22 -0
- package/src/domains/shared/runtime.ts +6 -3
- package/src/domains/shared/sampleFormatters.ts +47 -0
- package/src/domains/shared/semanticPackage.ts +96 -0
- package/src/domains/shared/suggestions/detectCrossDomain.test.ts +43 -0
- package/src/domains/shared/suggestions/detectCrossDomain.ts +116 -0
- package/src/domains/shared/suggestions/expandGlob.test.ts +92 -0
- package/src/domains/shared/suggestions/expandGlob.ts +51 -0
- package/src/domains/shared/suggestions/expandLookupQueries.ts +57 -0
- package/src/domains/shared/suggestions/index.ts +7 -0
- package/src/domains/shared/suggestions/listDomainNames.ts +61 -0
- package/src/domains/shared/suggestions/suggestNames.test.ts +70 -0
- package/src/domains/shared/suggestions/suggestNames.ts +97 -0
- package/src/domains/shared/types/index.ts +1 -0
- package/src/domains/shared/types/runtime.ts +4 -1
- package/src/domains/shared/types/sample.ts +17 -0
- package/src/domains/skill/helpers/resolveSkillSources.ts +49 -16
- package/src/domains/standard/commands/index.ts +2 -1
- package/src/domains/standard/commands/sample.ts +57 -0
- package/src/domains/standard/formatters/index.ts +2 -1
- package/src/domains/standard/formatters/sample.ts +15 -0
- package/src/domains/standard/index.ts +8 -1
- package/src/domains/standard/mcp/specs.ts +48 -1
- package/src/domains/standard/operations/index.ts +2 -1
- package/src/domains/standard/operations/lookup.ts +9 -0
- package/src/domains/standard/operations/sample.ts +35 -0
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +14 -2
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +33 -0
- package/src/domains/standard/orchestration/standardEmptyError.ts +16 -6
- package/src/domains/token/commands/index.ts +2 -1
- package/src/domains/token/commands/sample.ts +53 -0
- package/src/domains/token/formatters/index.ts +2 -1
- package/src/domains/token/formatters/sample.ts +16 -0
- package/src/domains/token/index.ts +8 -1
- package/src/domains/token/mcp/specs.ts +47 -1
- package/src/domains/token/operations/index.ts +2 -1
- package/src/domains/token/operations/lookup.ts +9 -0
- package/src/domains/token/operations/sample.ts +32 -0
- package/src/domains/token/orchestration/resolveTokenLookup.ts +14 -2
- package/src/domains/token/orchestration/tokenEmptyError.test.ts +33 -0
- package/src/domains/token/orchestration/tokenEmptyError.ts +5 -2
- package/src/domains/trace/commands/clear.ts +71 -0
- package/src/domains/trace/commands/index.ts +5 -0
- package/src/domains/trace/commands/sessions.ts +32 -0
- package/src/domains/trace/commands/show.ts +145 -0
- package/src/domains/trace/formatters/index.ts +9 -0
- package/src/domains/trace/formatters/sessions.ts +80 -0
- package/src/domains/trace/formatters/show.ts +122 -0
- package/src/domains/trace/index.ts +17 -0
- package/src/domains/trace/operations/index.ts +8 -0
- package/src/domains/trace/operations/listSessions.test.ts +55 -0
- package/src/domains/trace/operations/listSessions.ts +52 -0
- package/src/domains/trace/operations/readTraceLog.test.ts +100 -0
- package/src/domains/trace/operations/readTraceLog.ts +104 -0
- package/src/domains/trace/tracePlugin.test.ts +171 -0
- package/src/domains/trace/tracePlugin.ts +159 -0
- package/src/domains/trace/types.ts +65 -0
- package/src/error/PragmaError.ts +15 -2
- package/src/error/types.ts +15 -1
- package/src/mcp/error/serializeErrorPayload.ts +7 -0
- package/src/mcp/error/types.ts +8 -0
- package/src/mcp/registerResources.test.ts +38 -16
- package/src/mcp/registerResources.ts +57 -8
- package/src/mcp/tools/registerTools.test.ts +22 -7
- package/src/mcp/utils/wrapTool.test.ts +1 -0
- package/src/pipeline/collectCommands.ts +7 -1
- package/src/pipeline/renderError.ts +15 -0
- package/src/pipeline/resolveCommandKind.ts +8 -1
- package/src/pipeline/runCli.ts +12 -1
- package/src/testing/createTestMcpClient.ts +1 -0
- package/src/testing/graphqlFixtures.ts +72 -0
- package/src/testing/index.ts +7 -0
- package/src/testing/integration/mcp-surface.test.ts +11 -5
package/dist/pragma
CHANGED
|
Binary file
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@canonical/pragma-cli",
|
|
3
3
|
"description": "CLI and MCP server for Canonical's design system.",
|
|
4
|
-
"version": "0.
|
|
4
|
+
"version": "0.29.0",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "src/index.ts",
|
|
7
7
|
"types": "src/index.ts",
|
|
@@ -44,7 +44,7 @@
|
|
|
44
44
|
"access": "public"
|
|
45
45
|
},
|
|
46
46
|
"scripts": {
|
|
47
|
-
"build": "bun
|
|
47
|
+
"build": "bun run scripts/build.ts",
|
|
48
48
|
"build:compile": "bun build --compile --minify --target=bun-linux-x64 src/bin.ts --outfile dist/pragma",
|
|
49
49
|
"prepublishOnly": "bun run build",
|
|
50
50
|
"check": "bun run check:biome && bun run check:ts && bun run check:webarchitect",
|
|
@@ -60,9 +60,9 @@
|
|
|
60
60
|
},
|
|
61
61
|
"devDependencies": {
|
|
62
62
|
"@biomejs/biome": "2.4.9",
|
|
63
|
-
"@canonical/biome-config": "^0.
|
|
64
|
-
"@canonical/typescript-config": "^0.
|
|
65
|
-
"@canonical/webarchitect": "^0.
|
|
63
|
+
"@canonical/biome-config": "^0.29.0",
|
|
64
|
+
"@canonical/typescript-config": "^0.29.0",
|
|
65
|
+
"@canonical/webarchitect": "^0.29.0",
|
|
66
66
|
"@types/bun": "^1.3.10",
|
|
67
67
|
"@types/node": "^24.12.0",
|
|
68
68
|
"@types/react": "^19.2.14",
|
|
@@ -70,19 +70,18 @@
|
|
|
70
70
|
"typescript": "^5.9.3",
|
|
71
71
|
"vitest": "^4.0.18"
|
|
72
72
|
},
|
|
73
|
-
"
|
|
73
|
+
"dependencies": {
|
|
74
74
|
"@canonical/anatomy-dsl": "^0.2.2",
|
|
75
|
+
"@canonical/cli-core": "^0.29.0",
|
|
75
76
|
"@canonical/code-standards": "^0.1.2",
|
|
76
|
-
"@canonical/design-system": "^0.1.2"
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
"@canonical/
|
|
80
|
-
"@canonical/
|
|
81
|
-
"@canonical/
|
|
82
|
-
"@canonical/summon-
|
|
83
|
-
"@canonical/
|
|
84
|
-
"@canonical/summon-package": "^0.27.1-experimental.0",
|
|
85
|
-
"@canonical/task": "^0.27.1-experimental.0",
|
|
77
|
+
"@canonical/design-system": "^0.1.2",
|
|
78
|
+
"@canonical/harnesses": "^0.29.0",
|
|
79
|
+
"@canonical/ke": "^0.29.0",
|
|
80
|
+
"@canonical/ke-graphql": "^0.29.0",
|
|
81
|
+
"@canonical/summon-component": "^0.29.0",
|
|
82
|
+
"@canonical/summon-core": "^0.29.0",
|
|
83
|
+
"@canonical/summon-package": "^0.29.0",
|
|
84
|
+
"@canonical/task": "^0.29.0",
|
|
86
85
|
"@modelcontextprotocol/sdk": "^1.12.0",
|
|
87
86
|
"chalk": "^5.6.2",
|
|
88
87
|
"commander": "^14.0.3",
|
|
@@ -91,5 +90,5 @@
|
|
|
91
90
|
"react": "^19.2.4",
|
|
92
91
|
"zod": "^3.25.0"
|
|
93
92
|
},
|
|
94
|
-
"gitHead": "
|
|
93
|
+
"gitHead": "7c96ac31ff350a3bd099168d249680a8f9632568"
|
|
95
94
|
}
|
package/src/config/readConfig.ts
CHANGED
|
@@ -1,5 +1,10 @@
|
|
|
1
1
|
import { readFileSync } from "node:fs";
|
|
2
|
-
import {
|
|
2
|
+
import {
|
|
3
|
+
type Channel,
|
|
4
|
+
type Framework,
|
|
5
|
+
VALID_CHANNELS,
|
|
6
|
+
VALID_FRAMEWORKS,
|
|
7
|
+
} from "../constants.js";
|
|
3
8
|
import {
|
|
4
9
|
parsePackageEntry,
|
|
5
10
|
type RawPackageEntry,
|
|
@@ -18,6 +23,18 @@ function isValidChannel(value: unknown): value is Channel {
|
|
|
18
23
|
return typeof value === "string" && VALID_CHANNELS.includes(value as Channel);
|
|
19
24
|
}
|
|
20
25
|
|
|
26
|
+
/**
|
|
27
|
+
* Type guard: check whether a value is a recognized framework string.
|
|
28
|
+
*
|
|
29
|
+
* @param value - Candidate value.
|
|
30
|
+
* @returns `true` if value is a valid Framework.
|
|
31
|
+
*/
|
|
32
|
+
function isValidFramework(value: unknown): value is Framework {
|
|
33
|
+
return (
|
|
34
|
+
typeof value === "string" && VALID_FRAMEWORKS.includes(value as Framework)
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
21
38
|
/**
|
|
22
39
|
* Read pragma config from `pragma.config.json` in the given directory.
|
|
23
40
|
*
|
|
@@ -64,9 +81,28 @@ export default function readConfig(cwd: string = process.cwd()): PragmaConfig {
|
|
|
64
81
|
channel = parsed.channel;
|
|
65
82
|
}
|
|
66
83
|
|
|
84
|
+
const trace = typeof parsed.trace === "boolean" ? parsed.trace : undefined;
|
|
85
|
+
|
|
86
|
+
let framework: Framework | undefined;
|
|
87
|
+
if (parsed.framework !== undefined) {
|
|
88
|
+
if (!isValidFramework(parsed.framework)) {
|
|
89
|
+
throw PragmaError.configError(
|
|
90
|
+
`Invalid framework "${String(parsed.framework)}".`,
|
|
91
|
+
{ validOptions: [...VALID_FRAMEWORKS] },
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
framework = parsed.framework;
|
|
95
|
+
}
|
|
96
|
+
|
|
67
97
|
const packages = parsePackagesField(parsed.packages);
|
|
68
98
|
|
|
69
|
-
return
|
|
99
|
+
return {
|
|
100
|
+
tier,
|
|
101
|
+
channel,
|
|
102
|
+
...(packages ? { packages } : {}),
|
|
103
|
+
...(trace !== undefined ? { trace } : {}),
|
|
104
|
+
...(framework !== undefined ? { framework } : {}),
|
|
105
|
+
};
|
|
70
106
|
}
|
|
71
107
|
|
|
72
108
|
// ---------------------------------------------------------------------------
|
package/src/config/types.ts
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
* so consumers can import types without pulling in filesystem code.
|
|
6
6
|
*/
|
|
7
7
|
|
|
8
|
-
import type { Channel } from "../constants.js";
|
|
8
|
+
import type { Channel, Framework } from "../constants.js";
|
|
9
9
|
import type { RawPackageEntry } from "../domains/refs/operations/parseRef.js";
|
|
10
10
|
|
|
11
11
|
/** Parsed contents of pragma.config.json. */
|
|
@@ -20,6 +20,13 @@ interface PragmaConfig {
|
|
|
20
20
|
* When absent, the hardcoded defaults are used.
|
|
21
21
|
*/
|
|
22
22
|
packages?: ReadonlyArray<RawPackageEntry> | undefined;
|
|
23
|
+
/** Whether query access tracing is enabled. Overridden by PRAGMA_TRACE env var. */
|
|
24
|
+
trace?: boolean | undefined;
|
|
25
|
+
/**
|
|
26
|
+
* Preferred UI framework (`react` | `svelte` | `lit`). Currently advisory
|
|
27
|
+
* only — no behaviour reads it yet; reserved for future framework defaulting.
|
|
28
|
+
*/
|
|
29
|
+
framework?: Framework | undefined;
|
|
23
30
|
}
|
|
24
31
|
|
|
25
32
|
/** Partial update payload for writing config changes. */
|
|
@@ -30,6 +37,10 @@ interface ConfigUpdate {
|
|
|
30
37
|
channel?: Channel | undefined;
|
|
31
38
|
/** New packages list, `undefined` to remove the field. */
|
|
32
39
|
packages?: ReadonlyArray<RawPackageEntry> | undefined;
|
|
40
|
+
/** Enable/disable query tracing, `undefined` to remove the field. */
|
|
41
|
+
trace?: boolean | undefined;
|
|
42
|
+
/** New preferred framework, `undefined` to remove the field. */
|
|
43
|
+
framework?: Framework | undefined;
|
|
33
44
|
}
|
|
34
45
|
|
|
35
46
|
export type { ConfigUpdate, PragmaConfig };
|
|
@@ -50,6 +50,18 @@ export default function writeConfig(cwd: string, update: ConfigUpdate): void {
|
|
|
50
50
|
delete existing.packages;
|
|
51
51
|
}
|
|
52
52
|
|
|
53
|
+
if (update.trace !== undefined) {
|
|
54
|
+
existing.trace = update.trace;
|
|
55
|
+
} else if ("trace" in update) {
|
|
56
|
+
delete existing.trace;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
if (update.framework !== undefined) {
|
|
60
|
+
existing.framework = update.framework;
|
|
61
|
+
} else if ("framework" in update) {
|
|
62
|
+
delete existing.framework;
|
|
63
|
+
}
|
|
64
|
+
|
|
53
65
|
writeFileSync(tempPath, `${JSON.stringify(existing, null, 2)}\n`);
|
|
54
66
|
renameSync(tempPath, configPath);
|
|
55
67
|
}
|
package/src/constants.ts
CHANGED
|
@@ -24,6 +24,16 @@ const VALID_CHANNELS = ["normal", "experimental", "prerelease"] as const;
|
|
|
24
24
|
/** A release channel name. */
|
|
25
25
|
type Channel = (typeof VALID_CHANNELS)[number];
|
|
26
26
|
|
|
27
|
+
/** Allowed values for the `framework` config field. */
|
|
28
|
+
const VALID_FRAMEWORKS = ["react", "svelte", "lit"] as const;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* A preferred UI framework. Currently advisory only (a no-op); reserved for
|
|
32
|
+
* future use to default framework-specific behaviour (e.g. which generator
|
|
33
|
+
* `pragma`/`summon` scaffolds, which component tier is surfaced).
|
|
34
|
+
*/
|
|
35
|
+
type Framework = (typeof VALID_FRAMEWORKS)[number];
|
|
36
|
+
|
|
27
37
|
/**
|
|
28
38
|
* Per-namespace property URIs for semantically equivalent fields.
|
|
29
39
|
* Each ontology uses different property names for the same concept
|
|
@@ -68,11 +78,12 @@ const PROPERTY_MAP: Record<string, OntologyPropertyMap> = {
|
|
|
68
78
|
},
|
|
69
79
|
};
|
|
70
80
|
|
|
71
|
-
export type { Channel, OntologyPropertyMap };
|
|
81
|
+
export type { Channel, Framework, OntologyPropertyMap };
|
|
72
82
|
export {
|
|
73
83
|
PROGRAM_DESCRIPTION,
|
|
74
84
|
PROGRAM_NAME,
|
|
75
85
|
PROPERTY_MAP,
|
|
76
86
|
VALID_CHANNELS,
|
|
87
|
+
VALID_FRAMEWORKS,
|
|
77
88
|
VERSION,
|
|
78
89
|
};
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** @module Re-exports block command builders (`lookup`, `
|
|
1
|
+
/** @module Re-exports block command builders (`list`, `lookup`, `sample`). */
|
|
2
2
|
|
|
3
3
|
export { default as listCommand } from "./list.js";
|
|
4
4
|
export { default as lookupCommand } from "./lookup.js";
|
|
5
|
+
export { default as sampleCommand } from "./sample.js";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Wires the `pragma block sample` CLI command.
|
|
3
|
+
*
|
|
4
|
+
* Returns 1–5 complete block instances as exemplars so agents
|
|
5
|
+
* can learn the data shape before writing queries.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type CommandDefinition,
|
|
10
|
+
createOutputResult,
|
|
11
|
+
} from "@canonical/cli-core";
|
|
12
|
+
import type { PragmaContext } from "../../shared/context.js";
|
|
13
|
+
import { selectFormatter } from "../../shared/formatters.js";
|
|
14
|
+
import type { BlockDetailed, SampleOutput } from "../../shared/types/index.js";
|
|
15
|
+
import { sampleFormatters } from "../formatters/index.js";
|
|
16
|
+
import { sampleBlocks } from "../operations/index.js";
|
|
17
|
+
import { buildBlockFilters } from "../orchestration/index.js";
|
|
18
|
+
|
|
19
|
+
const NEXT_STEPS = [
|
|
20
|
+
"Use block_lookup to inspect specific blocks by name.",
|
|
21
|
+
"Use block_list to see all available blocks with filtering.",
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
export default function sampleCommand(ctx: PragmaContext): CommandDefinition {
|
|
25
|
+
return {
|
|
26
|
+
path: ["block", "sample"],
|
|
27
|
+
description:
|
|
28
|
+
"Return randomly selected complete block instances as exemplars for shape discovery",
|
|
29
|
+
parameters: [
|
|
30
|
+
{
|
|
31
|
+
name: "count",
|
|
32
|
+
type: "string",
|
|
33
|
+
description: "Number of samples to return (1–5, default 2)",
|
|
34
|
+
required: false,
|
|
35
|
+
},
|
|
36
|
+
],
|
|
37
|
+
meta: {
|
|
38
|
+
examples: ["pragma block sample", "pragma block sample --count 3"],
|
|
39
|
+
},
|
|
40
|
+
async execute(params) {
|
|
41
|
+
const count = Number(params.count ?? 2);
|
|
42
|
+
const filters = buildBlockFilters(ctx);
|
|
43
|
+
const result = await sampleBlocks(ctx.store, filters, count);
|
|
44
|
+
|
|
45
|
+
const output: SampleOutput<BlockDetailed> = {
|
|
46
|
+
...result,
|
|
47
|
+
nextSteps: NEXT_STEPS,
|
|
48
|
+
};
|
|
49
|
+
|
|
50
|
+
return createOutputResult(output, {
|
|
51
|
+
plain: selectFormatter(ctx, sampleFormatters),
|
|
52
|
+
});
|
|
53
|
+
},
|
|
54
|
+
};
|
|
55
|
+
}
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** @module Re-exports block formatters (`lookupFormatters`, `
|
|
1
|
+
/** @module Re-exports block formatters (`listFormatters`, `lookupFormatters`, `sampleFormatters`). */
|
|
2
2
|
|
|
3
3
|
export { default as listFormatters } from "./list.js";
|
|
4
4
|
export { createLookupOptions, default as lookupFormatters } from "./lookup.js";
|
|
5
|
+
export { default as sampleFormatters } from "./sample.js";
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Sample formatter for the block domain.
|
|
3
|
+
*
|
|
4
|
+
* Delegates to the shared factory, rendering each item via the
|
|
5
|
+
* existing block lookup formatter with all aspects enabled.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import createSampleFormatters from "../../shared/sampleFormatters.js";
|
|
9
|
+
import type { BlockDetailed } from "../../shared/types/index.js";
|
|
10
|
+
import lookupFormatters from "./lookup.js";
|
|
11
|
+
|
|
12
|
+
const ALL_ASPECTS = {
|
|
13
|
+
anatomy: true,
|
|
14
|
+
modifiers: true,
|
|
15
|
+
tokens: true,
|
|
16
|
+
implementations: true,
|
|
17
|
+
} as const;
|
|
18
|
+
|
|
19
|
+
export default createSampleFormatters<BlockDetailed>("blocks", (block, mode) =>
|
|
20
|
+
lookupFormatters[mode]({ block, detailed: true, aspects: ALL_ASPECTS }),
|
|
21
|
+
);
|
|
@@ -7,15 +7,15 @@
|
|
|
7
7
|
|
|
8
8
|
import type { CommandDefinition } from "@canonical/cli-core";
|
|
9
9
|
import type { PragmaContext } from "../shared/context.js";
|
|
10
|
-
import { listCommand, lookupCommand } from "./commands/index.js";
|
|
10
|
+
import { listCommand, lookupCommand, sampleCommand } from "./commands/index.js";
|
|
11
11
|
|
|
12
12
|
export function commands(ctx: PragmaContext): CommandDefinition[] {
|
|
13
|
-
return [listCommand(ctx), lookupCommand(ctx)];
|
|
13
|
+
return [listCommand(ctx), lookupCommand(ctx), sampleCommand(ctx)];
|
|
14
14
|
}
|
|
15
15
|
|
|
16
16
|
export { blockConfig } from "./blockConfig.js";
|
|
17
17
|
export { specs as mcpSpecs } from "./mcp/index.js";
|
|
18
|
-
export { listBlocks, lookupBlock } from "./operations/index.js";
|
|
18
|
+
export { listBlocks, lookupBlock, sampleBlocks } from "./operations/index.js";
|
|
19
19
|
export {
|
|
20
20
|
blockEmptyError,
|
|
21
21
|
buildBlockFilters,
|
|
@@ -9,7 +9,9 @@ import type { ToolSpec } from "../../shared/ToolSpec.js";
|
|
|
9
9
|
import {
|
|
10
10
|
listFormatters as blockListFmt,
|
|
11
11
|
lookupFormatters as blockLookupFmt,
|
|
12
|
+
sampleFormatters as blockSampleFmt,
|
|
12
13
|
} from "../formatters/index.js";
|
|
14
|
+
import { sampleBlocks } from "../operations/index.js";
|
|
13
15
|
import {
|
|
14
16
|
buildBlockFilters,
|
|
15
17
|
resolveBlockList,
|
|
@@ -194,6 +196,52 @@ const specs: readonly ToolSpec[] = [
|
|
|
194
196
|
return { data: result, meta: { count: result.results.length } };
|
|
195
197
|
},
|
|
196
198
|
},
|
|
199
|
+
{
|
|
200
|
+
name: "block_sample",
|
|
201
|
+
description:
|
|
202
|
+
"Return 1–5 randomly selected complete block instances as exemplars. Use BEFORE writing queries to see actual data shapes, property names, and value formats. Each call returns different instances.",
|
|
203
|
+
params: {
|
|
204
|
+
count: {
|
|
205
|
+
type: "string",
|
|
206
|
+
description: "Number of samples (1–5, default 2)",
|
|
207
|
+
optional: true,
|
|
208
|
+
},
|
|
209
|
+
condensed: {
|
|
210
|
+
type: "boolean",
|
|
211
|
+
description: "Token-optimized output",
|
|
212
|
+
optional: true,
|
|
213
|
+
},
|
|
214
|
+
},
|
|
215
|
+
readOnly: true,
|
|
216
|
+
async execute(rt, { count, condensed }) {
|
|
217
|
+
const n = Number(count ?? 2);
|
|
218
|
+
const filters = buildBlockFilters(rt);
|
|
219
|
+
const result = await sampleBlocks(rt.store, filters, n);
|
|
220
|
+
const nextSteps = [
|
|
221
|
+
`These are ${result.samples.length} of ${result.totalCount} total blocks.`,
|
|
222
|
+
"Use block_lookup to inspect specific blocks by name.",
|
|
223
|
+
"Use block_list to see all available blocks with filtering.",
|
|
224
|
+
];
|
|
225
|
+
|
|
226
|
+
if (condensed) {
|
|
227
|
+
const text = blockSampleFmt.llm({ ...result, nextSteps });
|
|
228
|
+
return {
|
|
229
|
+
condensed: true,
|
|
230
|
+
text,
|
|
231
|
+
tokens: `~${Math.ceil(text.length / 4)}`,
|
|
232
|
+
};
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
return {
|
|
236
|
+
data: {
|
|
237
|
+
samples: result.samples,
|
|
238
|
+
totalCount: result.totalCount,
|
|
239
|
+
nextSteps,
|
|
240
|
+
},
|
|
241
|
+
meta: { count: result.samples.length },
|
|
242
|
+
};
|
|
243
|
+
},
|
|
244
|
+
},
|
|
197
245
|
] as const;
|
|
198
246
|
|
|
199
247
|
export default specs;
|
|
@@ -1,4 +1,5 @@
|
|
|
1
|
-
/** @module Re-exports block operations (`lookupBlock`, `
|
|
1
|
+
/** @module Re-exports block operations (`listBlocks`, `lookupBlock`, `sampleBlocks`). */
|
|
2
2
|
|
|
3
3
|
export { default as listBlocks } from "./list.js";
|
|
4
4
|
export { default as lookupBlock } from "./lookup.js";
|
|
5
|
+
export { default as sampleBlocks } from "./sample.js";
|
|
@@ -21,6 +21,11 @@ import { buildQuery } from "../../shared/buildQuery.js";
|
|
|
21
21
|
import extractLocalName from "../../shared/extractLocalName.js";
|
|
22
22
|
import { buildFilters } from "../../shared/filters/buildFilters.js";
|
|
23
23
|
import { P } from "../../shared/prefixes.js";
|
|
24
|
+
import {
|
|
25
|
+
detectCrossDomain,
|
|
26
|
+
listDomainNames,
|
|
27
|
+
suggestNames,
|
|
28
|
+
} from "../../shared/suggestions/index.js";
|
|
24
29
|
import type {
|
|
25
30
|
BlockDetailed,
|
|
26
31
|
BlockSubcomponent,
|
|
@@ -80,12 +85,16 @@ export default async function lookupBlock(
|
|
|
80
85
|
);
|
|
81
86
|
|
|
82
87
|
if (baseResult.type !== "select" || baseResult.bindings.length === 0) {
|
|
88
|
+
const candidates = await listDomainNames(store, "block");
|
|
89
|
+
const crossDomain = await detectCrossDomain(nameOrUri, "block", store);
|
|
83
90
|
throw PragmaError.notFound("block", nameOrUri, {
|
|
91
|
+
suggestions: suggestNames(nameOrUri, candidates),
|
|
84
92
|
recovery: {
|
|
85
93
|
message: "List available blocks.",
|
|
86
94
|
cli: "pragma block list",
|
|
87
95
|
mcp: { tool: "block_list" },
|
|
88
96
|
},
|
|
97
|
+
crossDomain: crossDomain ?? undefined,
|
|
89
98
|
});
|
|
90
99
|
}
|
|
91
100
|
|
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns a small number of complete block instances as exemplars.
|
|
3
|
+
*
|
|
4
|
+
* Composes the existing list and lookup operations to avoid duplicating
|
|
5
|
+
* any SPARQL logic. Agents use this to learn the data shape before querying.
|
|
6
|
+
*
|
|
7
|
+
* @param store - ke store to query
|
|
8
|
+
* @param filters - tier and channel filter configuration
|
|
9
|
+
* @param count - number of samples (clamped 1–5, default 2)
|
|
10
|
+
* @returns sample result with detailed blocks and total count
|
|
11
|
+
* @note Queries ke store
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import type { Store } from "@canonical/ke";
|
|
15
|
+
import pickRandom from "../../shared/pickRandom.js";
|
|
16
|
+
import type {
|
|
17
|
+
BlockDetailed,
|
|
18
|
+
FilterConfig,
|
|
19
|
+
SampleResult,
|
|
20
|
+
} from "../../shared/types/index.js";
|
|
21
|
+
import listBlocks from "./list.js";
|
|
22
|
+
import lookupBlock from "./lookup.js";
|
|
23
|
+
|
|
24
|
+
export default async function sampleBlocks(
|
|
25
|
+
store: Store,
|
|
26
|
+
filters: FilterConfig,
|
|
27
|
+
count = 2,
|
|
28
|
+
): Promise<SampleResult<BlockDetailed>> {
|
|
29
|
+
const clamped = Math.max(1, Math.min(5, count));
|
|
30
|
+
const all = await listBlocks(store, filters);
|
|
31
|
+
const selected = pickRandom(all, clamped);
|
|
32
|
+
|
|
33
|
+
const samples = await Promise.all(
|
|
34
|
+
selected.map(async (b) => {
|
|
35
|
+
const results = await lookupBlock(store, b.name, filters);
|
|
36
|
+
return results[0] as BlockDetailed;
|
|
37
|
+
}),
|
|
38
|
+
);
|
|
39
|
+
|
|
40
|
+
return { samples, totalCount: all.length };
|
|
41
|
+
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { FilterConfig } from "../../shared/types/index.js";
|
|
3
|
+
import blockEmptyError from "./blockEmptyError.js";
|
|
4
|
+
|
|
5
|
+
const filters: FilterConfig = { tier: "Apps/WPE", channel: "normal" };
|
|
6
|
+
|
|
7
|
+
describe("blockEmptyError", () => {
|
|
8
|
+
it("filter-narrowing: a tier was active → widen to all tiers", () => {
|
|
9
|
+
const err = blockEmptyError(filters, false);
|
|
10
|
+
|
|
11
|
+
expect(err.filters).toEqual({ tier: "Apps/WPE", channel: "normal" });
|
|
12
|
+
expect(err.recovery).toEqual({
|
|
13
|
+
message: "Widen the search to show all tiers.",
|
|
14
|
+
cli: "pragma block list --all-tiers",
|
|
15
|
+
mcp: { tool: "block_list", params: { allTiers: true } },
|
|
16
|
+
});
|
|
17
|
+
});
|
|
18
|
+
|
|
19
|
+
it("honest terminal: already at all-tiers → no recovery (no self-loop)", () => {
|
|
20
|
+
const err = blockEmptyError({ tier: undefined, channel: "normal" }, true);
|
|
21
|
+
|
|
22
|
+
// The widest tier query already returned empty; suggesting --all-tiers
|
|
23
|
+
// again would loop. recovery: undefined is the correct terminal state.
|
|
24
|
+
expect(err.recovery).toBeUndefined();
|
|
25
|
+
});
|
|
26
|
+
});
|
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import type { LookupContract, LookupResult } from "../../shared/contracts.js";
|
|
2
2
|
import lookupMany from "../../shared/lookupMany.js";
|
|
3
3
|
import type { PragmaRuntime } from "../../shared/runtime.js";
|
|
4
|
+
import { expandLookupQueries } from "../../shared/suggestions/index.js";
|
|
4
5
|
import type { BlockDetailed, FilterConfig } from "../../shared/types/index.js";
|
|
5
6
|
import { lookupBlock } from "../operations/index.js";
|
|
6
7
|
|
|
@@ -9,15 +10,21 @@ export default async function resolveBlockLookup(
|
|
|
9
10
|
queries: readonly string[],
|
|
10
11
|
filters: FilterConfig,
|
|
11
12
|
): Promise<LookupContract<BlockDetailed>> {
|
|
12
|
-
const
|
|
13
|
+
const { names, globErrors } = await expandLookupQueries(
|
|
13
14
|
queries,
|
|
14
|
-
|
|
15
|
+
store,
|
|
16
|
+
"block",
|
|
15
17
|
);
|
|
16
18
|
|
|
19
|
+
const nested: LookupResult<BlockDetailed[]> =
|
|
20
|
+
names.length > 0
|
|
21
|
+
? await lookupMany(names, (query) => lookupBlock(store, query, filters))
|
|
22
|
+
: { results: [], errors: [] };
|
|
23
|
+
|
|
17
24
|
// lookupBlock returns BlockDetailed[] (multiple matches per name) — flatten
|
|
18
25
|
const result: LookupResult<BlockDetailed> = {
|
|
19
26
|
results: nested.results.flat(),
|
|
20
|
-
errors: nested.errors,
|
|
27
|
+
errors: [...nested.errors, ...globErrors],
|
|
21
28
|
};
|
|
22
29
|
|
|
23
30
|
return {
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
import { mkdtempSync, readFileSync, rmSync, writeFileSync } from "node:fs";
|
|
2
|
+
import { tmpdir } from "node:os";
|
|
3
|
+
import { join } from "node:path";
|
|
4
|
+
import type { Store } from "@canonical/ke";
|
|
5
|
+
import { afterEach, beforeEach, describe, expect, it } from "vitest";
|
|
6
|
+
import { readConfig } from "#config";
|
|
7
|
+
import type { PragmaContext } from "../../shared/context.js";
|
|
8
|
+
import buildFrameworkCommand from "./framework.js";
|
|
9
|
+
|
|
10
|
+
function makeCtx(cwd: string): PragmaContext {
|
|
11
|
+
return {
|
|
12
|
+
cwd,
|
|
13
|
+
globalFlags: { llm: false, format: "text" as const, verbose: false },
|
|
14
|
+
store: {} as Store,
|
|
15
|
+
config: { tier: undefined, channel: "normal" },
|
|
16
|
+
};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
describe("config framework command", () => {
|
|
20
|
+
let dir: string;
|
|
21
|
+
|
|
22
|
+
beforeEach(() => {
|
|
23
|
+
dir = mkdtempSync(join(tmpdir(), "pragma-cmd-framework-"));
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
afterEach(() => {
|
|
27
|
+
rmSync(dir, { recursive: true, force: true });
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it("sets framework to react", async () => {
|
|
31
|
+
const ctx = makeCtx(dir);
|
|
32
|
+
const result = await buildFrameworkCommand(ctx).execute(
|
|
33
|
+
{ value: "react" },
|
|
34
|
+
ctx,
|
|
35
|
+
);
|
|
36
|
+
expect(result.tag).toBe("output");
|
|
37
|
+
expect(readConfig(dir).framework).toBe("react");
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
it("accepts svelte and lit", async () => {
|
|
41
|
+
const ctx = makeCtx(dir);
|
|
42
|
+
await buildFrameworkCommand(ctx).execute({ value: "svelte" }, ctx);
|
|
43
|
+
expect(readConfig(dir).framework).toBe("svelte");
|
|
44
|
+
await buildFrameworkCommand(ctx).execute({ value: "lit" }, ctx);
|
|
45
|
+
expect(readConfig(dir).framework).toBe("lit");
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
it("rejects an invalid framework", async () => {
|
|
49
|
+
const ctx = makeCtx(dir);
|
|
50
|
+
await expect(
|
|
51
|
+
buildFrameworkCommand(ctx).execute({ value: "angular" }, ctx),
|
|
52
|
+
).rejects.toThrow(/framework/i);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("reports 'none' when unset", async () => {
|
|
56
|
+
const ctx = makeCtx(dir);
|
|
57
|
+
const result = await buildFrameworkCommand(ctx).execute({}, ctx);
|
|
58
|
+
expect(result.tag).toBe("output");
|
|
59
|
+
if (result.tag === "output") {
|
|
60
|
+
expect(result.value).toBe("none");
|
|
61
|
+
}
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
it("resets framework via --reset (removes the field)", async () => {
|
|
65
|
+
writeFileSync(join(dir, "pragma.config.json"), '{"framework":"react"}');
|
|
66
|
+
const ctx = makeCtx(dir);
|
|
67
|
+
await buildFrameworkCommand(ctx).execute({ reset: true }, ctx);
|
|
68
|
+
expect(readConfig(dir).framework).toBeUndefined();
|
|
69
|
+
// The field is removed, not left as a stale value.
|
|
70
|
+
const raw = JSON.parse(
|
|
71
|
+
readFileSync(join(dir, "pragma.config.json"), "utf-8"),
|
|
72
|
+
) as Record<string, unknown>;
|
|
73
|
+
expect(raw).not.toHaveProperty("framework");
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
it("preserves other config fields when setting framework", async () => {
|
|
77
|
+
writeFileSync(
|
|
78
|
+
join(dir, "pragma.config.json"),
|
|
79
|
+
JSON.stringify({ tier: "Apps/WPE", trace: true }),
|
|
80
|
+
);
|
|
81
|
+
const ctx = makeCtx(dir);
|
|
82
|
+
await buildFrameworkCommand(ctx).execute({ value: "lit" }, ctx);
|
|
83
|
+
const config = readConfig(dir);
|
|
84
|
+
expect(config.framework).toBe("lit");
|
|
85
|
+
expect(config.tier).toBe("Apps/WPE");
|
|
86
|
+
expect(config.trace).toBe(true);
|
|
87
|
+
});
|
|
88
|
+
});
|