@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
|
@@ -157,8 +157,11 @@ describe("updateRefs", () => {
|
|
|
157
157
|
|
|
158
158
|
it("uses defaults when no config exists", async () => {
|
|
159
159
|
const results = await updateRefs({ cwd: projectDir });
|
|
160
|
-
// All
|
|
161
|
-
expect(results
|
|
162
|
-
|
|
160
|
+
// All 3 defaults are git refs
|
|
161
|
+
expect(results).toHaveLength(3);
|
|
162
|
+
for (const r of results) {
|
|
163
|
+
// Each result is either cloned, up-to-date, or error (network)
|
|
164
|
+
expect(["cloned", "updated", "up-to-date", "error"]).toContain(r.kind);
|
|
165
|
+
}
|
|
163
166
|
});
|
|
164
167
|
});
|
|
@@ -152,8 +152,9 @@ function mergeEntries(
|
|
|
152
152
|
|
|
153
153
|
const merged = new Map<string, RawPackageEntry>();
|
|
154
154
|
|
|
155
|
-
for (const
|
|
156
|
-
|
|
155
|
+
for (const entry of DEFAULT_PACKAGES) {
|
|
156
|
+
const name = typeof entry === "string" ? entry : entry.name;
|
|
157
|
+
merged.set(name, entry);
|
|
157
158
|
}
|
|
158
159
|
for (const entry of globalEntries) {
|
|
159
160
|
const name = typeof entry === "string" ? entry : entry.name;
|
|
@@ -1,85 +1,124 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Boot a ke store from
|
|
2
|
+
* Boot a ke store from resolved semantic packages.
|
|
3
3
|
*
|
|
4
|
-
*
|
|
5
|
-
*
|
|
4
|
+
* Resolves packages via the loader chain (local > git > bundled),
|
|
5
|
+
* then loads all graph content into the store.
|
|
6
6
|
*
|
|
7
7
|
* @note Impure — reads filesystem, creates ke store.
|
|
8
8
|
*/
|
|
9
9
|
|
|
10
|
-
import {
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
10
|
+
import {
|
|
11
|
+
createStore,
|
|
12
|
+
definePlugin,
|
|
13
|
+
type Plugin,
|
|
14
|
+
type SourceSpec,
|
|
15
|
+
type Store,
|
|
16
|
+
} from "@canonical/ke";
|
|
14
17
|
import { PragmaError } from "../../error/index.js";
|
|
15
18
|
import type { PackageRef } from "../refs/operations/parseRef.js";
|
|
16
|
-
import {
|
|
19
|
+
import { parsePackageEntry } from "../refs/operations/parseRef.js";
|
|
20
|
+
import {
|
|
21
|
+
createBundledLoader,
|
|
22
|
+
createGitLoader,
|
|
23
|
+
createLocalLoader,
|
|
24
|
+
} from "./loaders/index.js";
|
|
25
|
+
import { DEFAULT_PACKAGES } from "./packages.js";
|
|
17
26
|
import { PREFIX_MAP } from "./prefixes.js";
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
* Convention-based TTL directories to scan in each package.
|
|
21
|
-
* Only directories that exist are globbed — packages that lack
|
|
22
|
-
* a `data/` or `definitions/` dir are silently skipped.
|
|
23
|
-
*/
|
|
24
|
-
const TTL_DIRS: readonly { dir: string; glob: string }[] = [
|
|
25
|
-
{ dir: "definitions", glob: "definitions/**/*.ttl" },
|
|
26
|
-
{ dir: "data", glob: "data/**/*.ttl" },
|
|
27
|
-
];
|
|
28
|
-
|
|
29
|
-
/**
|
|
30
|
-
* Resolve default TTL sources from resolved packages.
|
|
31
|
-
*
|
|
32
|
-
* Uses convention-based TTL discovery: definitions and data globs
|
|
33
|
-
* relative to each package root. Only adds globs for directories
|
|
34
|
-
* that actually exist in the package.
|
|
35
|
-
*
|
|
36
|
-
* @param refs - Parsed package references. Omit for defaults.
|
|
37
|
-
*/
|
|
38
|
-
export function defaultSources(refs?: ReadonlyArray<PackageRef>): SourceSpec[] {
|
|
39
|
-
const sources: SourceSpec[] = [];
|
|
40
|
-
const resolved = resolvePackages(refs);
|
|
41
|
-
|
|
42
|
-
for (const { dir } of resolved) {
|
|
43
|
-
for (const entry of TTL_DIRS) {
|
|
44
|
-
if (existsSync(join(dir, entry.dir))) {
|
|
45
|
-
sources.push(join(dir, entry.glob));
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
return sources;
|
|
51
|
-
}
|
|
27
|
+
import type { GraphContent, SemanticPackage } from "./semanticPackage.js";
|
|
28
|
+
import { resolveSemanticPackages } from "./semanticPackage.js";
|
|
52
29
|
|
|
53
30
|
export interface BootStoreOptions {
|
|
54
|
-
/** Override sources (skip
|
|
31
|
+
/** Override sources (skip package resolution — for testing). */
|
|
55
32
|
sources?: SourceSpec[];
|
|
56
33
|
/** Working directory for resolving relative paths. */
|
|
57
34
|
cwd?: string;
|
|
58
35
|
/** Cache path for serialized store. */
|
|
59
36
|
cache?: string;
|
|
60
|
-
/** Parsed package references for
|
|
37
|
+
/** Parsed package references for loader-based resolution. */
|
|
61
38
|
refs?: ReadonlyArray<PackageRef>;
|
|
39
|
+
/** Enable query tracing (from config). Env var PRAGMA_TRACE=1 overrides. */
|
|
40
|
+
trace?: boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
export interface BootResult {
|
|
44
|
+
/** The initialized ke store. */
|
|
45
|
+
readonly store: Store;
|
|
46
|
+
/** Resolved semantic packages (empty when sources override is used). */
|
|
47
|
+
readonly packages: readonly SemanticPackage[];
|
|
62
48
|
}
|
|
63
49
|
|
|
64
50
|
/**
|
|
65
51
|
* Boot a ke store from configuration.
|
|
66
52
|
*
|
|
53
|
+
* When `sources` is provided, uses them directly (testing path).
|
|
54
|
+
* Otherwise, resolves packages via the loader chain and loads their
|
|
55
|
+
* graph content into the store.
|
|
56
|
+
*
|
|
67
57
|
* @throws PragmaError with code STORE_ERROR on failure.
|
|
68
58
|
*/
|
|
69
59
|
export async function bootStore(
|
|
70
60
|
options: BootStoreOptions = {},
|
|
71
|
-
): Promise<
|
|
72
|
-
const sources = options.sources ?? defaultSources(options.refs);
|
|
73
|
-
|
|
61
|
+
): Promise<BootResult> {
|
|
74
62
|
try {
|
|
63
|
+
// Testing/programmatic override — use explicit sources
|
|
64
|
+
if (options.sources) {
|
|
65
|
+
const store = await createStore({
|
|
66
|
+
sources: options.sources,
|
|
67
|
+
prefixes: PREFIX_MAP,
|
|
68
|
+
cache: options.cache,
|
|
69
|
+
cwd: options.cwd,
|
|
70
|
+
});
|
|
71
|
+
return { store, packages: [] };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
// Resolve packages via loader chain: local > git > bundled
|
|
75
|
+
const refs =
|
|
76
|
+
options.refs && options.refs.length > 0
|
|
77
|
+
? options.refs
|
|
78
|
+
: DEFAULT_PACKAGES.map(parsePackageEntry);
|
|
79
|
+
// Precedence: local (file:// + node_modules) > git cache > bundled
|
|
80
|
+
const loaders = [
|
|
81
|
+
createLocalLoader(),
|
|
82
|
+
createGitLoader(),
|
|
83
|
+
createBundledLoader(),
|
|
84
|
+
];
|
|
85
|
+
const packages = await resolveSemanticPackages(refs, loaders);
|
|
86
|
+
|
|
87
|
+
// Collect all graph content from resolved packages
|
|
88
|
+
const allGraphs = packages.flatMap((pkg) => pkg.graphs);
|
|
89
|
+
|
|
90
|
+
// Use a ke plugin to load graph content during store creation.
|
|
91
|
+
// The onReady hook has access to ctx.load() which loads raw content.
|
|
92
|
+
const graphLoaderPlugin = definePlugin({
|
|
93
|
+
name: "pragma-graph-loader",
|
|
94
|
+
onReady(ctx) {
|
|
95
|
+
for (const graph of allGraphs) {
|
|
96
|
+
ctx.load(graph.content, { format: graph.format });
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
});
|
|
100
|
+
|
|
101
|
+
// biome-ignore lint: Plugin generic variance requires explicit unknown
|
|
102
|
+
const plugins: Plugin<any>[] = [graphLoaderPlugin];
|
|
103
|
+
const traceEnabled =
|
|
104
|
+
process.env.PRAGMA_TRACE === "1" || options.trace === true;
|
|
105
|
+
if (traceEnabled) {
|
|
106
|
+
const { createTracePlugin } = await import("../trace/tracePlugin.js");
|
|
107
|
+
const { traceDir } = await import("../refs/operations/paths.js");
|
|
108
|
+
plugins.push(createTracePlugin({ traceDir: traceDir() }));
|
|
109
|
+
}
|
|
110
|
+
|
|
75
111
|
const store = await createStore({
|
|
76
|
-
sources,
|
|
112
|
+
sources: [],
|
|
77
113
|
prefixes: PREFIX_MAP,
|
|
78
114
|
cache: options.cache,
|
|
79
115
|
cwd: options.cwd,
|
|
116
|
+
plugins,
|
|
80
117
|
});
|
|
81
|
-
|
|
118
|
+
|
|
119
|
+
return { store, packages };
|
|
82
120
|
} catch (error) {
|
|
121
|
+
if (error instanceof PragmaError) throw error;
|
|
83
122
|
throw PragmaError.storeError(
|
|
84
123
|
error instanceof Error ? error.message : String(error),
|
|
85
124
|
{
|
|
@@ -0,0 +1,109 @@
|
|
|
1
|
+
import { describe, expect, it } from "vitest";
|
|
2
|
+
import type { PragmaError } from "#error";
|
|
3
|
+
import blockSpecs from "../block/mcp/specs.js";
|
|
4
|
+
import blockEmptyError from "../block/orchestration/blockEmptyError.js";
|
|
5
|
+
import modifierSpecs from "../modifier/mcp/specs.js";
|
|
6
|
+
import modifierEmptyError from "../modifier/orchestration/modifierEmptyError.js";
|
|
7
|
+
import standardSpecs from "../standard/mcp/specs.js";
|
|
8
|
+
import standardEmptyError from "../standard/orchestration/standardEmptyError.js";
|
|
9
|
+
import tokenSpecs from "../token/mcp/specs.js";
|
|
10
|
+
import tokenEmptyError from "../token/orchestration/tokenEmptyError.js";
|
|
11
|
+
import type { FilterConfig } from "./types/index.js";
|
|
12
|
+
|
|
13
|
+
const filters: FilterConfig = { tier: "Apps/WPE", channel: "normal" };
|
|
14
|
+
const allTiers: FilterConfig = { tier: undefined, channel: "normal" };
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Every distinct empty-result scenario each domain can produce, paired with the
|
|
18
|
+
* set of MCP tool names that domain actually registers. The invariant below
|
|
19
|
+
* holds the four hints to the S-grade contract so authored consistency cannot
|
|
20
|
+
* silently re-drift.
|
|
21
|
+
*/
|
|
22
|
+
const cases: ReadonlyArray<{
|
|
23
|
+
domain: string;
|
|
24
|
+
error: PragmaError;
|
|
25
|
+
toolNames: ReadonlySet<string>;
|
|
26
|
+
// store-empty (packages absent) → the install rung, which must NOT carry an
|
|
27
|
+
// mcp retry (re-listing an absent store loops). filter-narrowing → may carry
|
|
28
|
+
// a list-retry mcp pointer.
|
|
29
|
+
storeEmpty: boolean;
|
|
30
|
+
}> = [
|
|
31
|
+
{
|
|
32
|
+
domain: "block (filter active)",
|
|
33
|
+
error: blockEmptyError(filters, false),
|
|
34
|
+
toolNames: new Set(blockSpecs.map((s) => s.name)),
|
|
35
|
+
storeEmpty: false,
|
|
36
|
+
},
|
|
37
|
+
{
|
|
38
|
+
domain: "block (all tiers — terminal)",
|
|
39
|
+
error: blockEmptyError(allTiers, true),
|
|
40
|
+
toolNames: new Set(blockSpecs.map((s) => s.name)),
|
|
41
|
+
storeEmpty: false,
|
|
42
|
+
},
|
|
43
|
+
{
|
|
44
|
+
domain: "token (category active)",
|
|
45
|
+
error: tokenEmptyError("color"),
|
|
46
|
+
toolNames: new Set(tokenSpecs.map((s) => s.name)),
|
|
47
|
+
storeEmpty: false,
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
domain: "token (store-empty)",
|
|
51
|
+
error: tokenEmptyError(),
|
|
52
|
+
toolNames: new Set(tokenSpecs.map((s) => s.name)),
|
|
53
|
+
storeEmpty: true,
|
|
54
|
+
},
|
|
55
|
+
{
|
|
56
|
+
domain: "modifier (store-empty)",
|
|
57
|
+
error: modifierEmptyError(),
|
|
58
|
+
toolNames: new Set(modifierSpecs.map((s) => s.name)),
|
|
59
|
+
storeEmpty: true,
|
|
60
|
+
},
|
|
61
|
+
{
|
|
62
|
+
domain: "standard (filter active)",
|
|
63
|
+
error: standardEmptyError({ category: "react" }),
|
|
64
|
+
toolNames: new Set(standardSpecs.map((s) => s.name)),
|
|
65
|
+
storeEmpty: false,
|
|
66
|
+
},
|
|
67
|
+
{
|
|
68
|
+
domain: "standard (store-empty)",
|
|
69
|
+
error: standardEmptyError({}),
|
|
70
|
+
toolNames: new Set(standardSpecs.map((s) => s.name)),
|
|
71
|
+
storeEmpty: true,
|
|
72
|
+
},
|
|
73
|
+
];
|
|
74
|
+
|
|
75
|
+
describe("empty-error recovery contract (cross-domain)", () => {
|
|
76
|
+
it.each(cases)("$domain honours the S-grade recovery shape", ({
|
|
77
|
+
error,
|
|
78
|
+
toolNames,
|
|
79
|
+
storeEmpty,
|
|
80
|
+
}) => {
|
|
81
|
+
const recovery = error.recovery;
|
|
82
|
+
|
|
83
|
+
// Terminal state (e.g. block already at all-tiers) is allowed.
|
|
84
|
+
if (recovery === undefined) return;
|
|
85
|
+
|
|
86
|
+
// Every hint carries a human-readable message.
|
|
87
|
+
expect(recovery.message).toBeTruthy();
|
|
88
|
+
|
|
89
|
+
// No hint is message-only: a runnable cli is always present (either a
|
|
90
|
+
// pragma re-list for filter-narrowing, or a bun add for store-empty).
|
|
91
|
+
expect(recovery.cli).toBeTruthy();
|
|
92
|
+
|
|
93
|
+
if (storeEmpty) {
|
|
94
|
+
// Store-empty must NOT carry an mcp retry: re-listing an absent store
|
|
95
|
+
// just loops. This is the anti-drift guard for the install rung.
|
|
96
|
+
expect(recovery.mcp).toBeUndefined();
|
|
97
|
+
} else if (recovery.mcp !== undefined) {
|
|
98
|
+
// A filter-narrowing mcp pointer must name a tool the domain actually
|
|
99
|
+
// registers — catches renamed/dangling tool references.
|
|
100
|
+
expect(toolNames.has(recovery.mcp.tool)).toBe(true);
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
it("never points an install hint at @canonical/ds-global (not a real default)", () => {
|
|
105
|
+
for (const { error } of cases) {
|
|
106
|
+
expect(error.recovery?.cli ?? "").not.toContain("ds-global");
|
|
107
|
+
}
|
|
108
|
+
});
|
|
109
|
+
});
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Bundled package loader for compiled binaries.
|
|
3
|
+
*
|
|
4
|
+
* Resolves semantic content from `Bun.embeddedFiles` — TTL and skill
|
|
5
|
+
* files embedded into the compiled binary via the build command.
|
|
6
|
+
*
|
|
7
|
+
* Blob names are hashed by bun (e.g. `button-a1b2c3d4.ttl`), losing
|
|
8
|
+
* directory and package association. The loader therefore returns a
|
|
9
|
+
* single SemanticPackage containing ALL embedded TTL content. RDF
|
|
10
|
+
* stores deduplicate triples, so this is safe when combined with
|
|
11
|
+
* higher-precedence loaders that resolve individual packages.
|
|
12
|
+
*
|
|
13
|
+
* @note Impure — reads embedded blobs.
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
import type { PackageRef } from "../../refs/operations/parseRef.js";
|
|
17
|
+
import type {
|
|
18
|
+
GraphContent,
|
|
19
|
+
PackageLoader,
|
|
20
|
+
SemanticPackage,
|
|
21
|
+
} from "../semanticPackage.js";
|
|
22
|
+
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
// State
|
|
25
|
+
// ---------------------------------------------------------------------------
|
|
26
|
+
|
|
27
|
+
/** Cached result — embedded content is immutable, no need to re-read. */
|
|
28
|
+
let cached: SemanticPackage | undefined;
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Public API
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export default function createBundledLoader(): PackageLoader {
|
|
35
|
+
return {
|
|
36
|
+
name: "bundled",
|
|
37
|
+
async resolve(_ref: PackageRef): Promise<SemanticPackage | undefined> {
|
|
38
|
+
if (cached) return cached;
|
|
39
|
+
|
|
40
|
+
if (typeof globalThis.Bun === "undefined") return undefined;
|
|
41
|
+
// Bun.embeddedFiles blobs have a .name property at runtime
|
|
42
|
+
const blobs = (globalThis.Bun as { embeddedFiles?: readonly Blob[] })
|
|
43
|
+
.embeddedFiles as ReadonlyArray<Blob & { name: string }> | undefined;
|
|
44
|
+
if (!blobs?.length) return undefined;
|
|
45
|
+
|
|
46
|
+
const graphs = await readEmbeddedGraphs(blobs);
|
|
47
|
+
if (graphs.length === 0) return undefined;
|
|
48
|
+
|
|
49
|
+
const version = await readEmbeddedVersion(blobs);
|
|
50
|
+
|
|
51
|
+
cached = {
|
|
52
|
+
name: "(bundled)",
|
|
53
|
+
version,
|
|
54
|
+
source: "bundled",
|
|
55
|
+
graphs,
|
|
56
|
+
skills: [],
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
return cached;
|
|
60
|
+
},
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
// ---------------------------------------------------------------------------
|
|
65
|
+
// Graph reading
|
|
66
|
+
// ---------------------------------------------------------------------------
|
|
67
|
+
|
|
68
|
+
async function readEmbeddedGraphs(
|
|
69
|
+
blobs: ReadonlyArray<Blob & { name: string }>,
|
|
70
|
+
): Promise<GraphContent[]> {
|
|
71
|
+
const graphs: GraphContent[] = [];
|
|
72
|
+
|
|
73
|
+
for (const blob of blobs) {
|
|
74
|
+
if (!blob.name.endsWith(".ttl")) continue;
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
const content = await blob.text();
|
|
78
|
+
graphs.push({
|
|
79
|
+
path: `(bundled)/${blob.name}`,
|
|
80
|
+
content,
|
|
81
|
+
format: "turtle",
|
|
82
|
+
});
|
|
83
|
+
} catch {}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
return graphs;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// ---------------------------------------------------------------------------
|
|
90
|
+
// Version reading
|
|
91
|
+
// ---------------------------------------------------------------------------
|
|
92
|
+
|
|
93
|
+
async function readEmbeddedVersion(
|
|
94
|
+
blobs: ReadonlyArray<Blob & { name: string }>,
|
|
95
|
+
): Promise<string> {
|
|
96
|
+
for (const blob of blobs) {
|
|
97
|
+
if (!blob.name.includes("package") || !blob.name.endsWith(".json"))
|
|
98
|
+
continue;
|
|
99
|
+
|
|
100
|
+
try {
|
|
101
|
+
const text = await blob.text();
|
|
102
|
+
const parsed = JSON.parse(text) as { version?: string };
|
|
103
|
+
if (parsed.version) return parsed.version;
|
|
104
|
+
} catch {}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
return "0.0.0";
|
|
108
|
+
}
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Git cache package loader.
|
|
3
|
+
*
|
|
4
|
+
* Resolves semantic packages from the git ref cache at
|
|
5
|
+
* `~/.cache/pragma/refs/<pkg>/<ref>/`, populated by `pragma update-refs`.
|
|
6
|
+
*
|
|
7
|
+
* Only handles `git` kind refs. File and npm refs are skipped.
|
|
8
|
+
*
|
|
9
|
+
* @note Impure — reads filesystem.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { existsSync } from "node:fs";
|
|
13
|
+
import type { PackageRef } from "../../refs/operations/parseRef.js";
|
|
14
|
+
import { gitCacheDir } from "../../refs/operations/paths.js";
|
|
15
|
+
import type { PackageLoader, SemanticPackage } from "../semanticPackage.js";
|
|
16
|
+
import readPackageDir from "./readPackageDir.js";
|
|
17
|
+
|
|
18
|
+
export default function createGitLoader(): PackageLoader {
|
|
19
|
+
return {
|
|
20
|
+
name: "git",
|
|
21
|
+
resolve(ref: PackageRef): SemanticPackage | undefined {
|
|
22
|
+
if (ref.kind !== "git") return undefined;
|
|
23
|
+
|
|
24
|
+
const dir = gitCacheDir(ref.pkg, ref.ref);
|
|
25
|
+
if (!existsSync(dir)) return undefined;
|
|
26
|
+
|
|
27
|
+
const { version, graphs, skills } = readPackageDir(dir);
|
|
28
|
+
return { name: ref.pkg, version, source: "git", graphs, skills };
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
/** @module Semantic package loaders — local, git, and bundled resolution. */
|
|
2
|
+
|
|
3
|
+
export { default as createBundledLoader } from "./bundledLoader.js";
|
|
4
|
+
export { default as createGitLoader } from "./gitLoader.js";
|
|
5
|
+
export { default as createLocalLoader } from "./localLoader.js";
|
|
6
|
+
export { default as readPackageDir } from "./readPackageDir.js";
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local package loader.
|
|
3
|
+
*
|
|
4
|
+
* Resolves semantic packages from the local filesystem:
|
|
5
|
+
* - `file://` refs → direct path check
|
|
6
|
+
* - All ref kinds → upward `node_modules` walk from this module's location
|
|
7
|
+
*
|
|
8
|
+
* The node_modules walk avoids `require.resolve('pkg/package.json')` which
|
|
9
|
+
* breaks for packages with strict `exports` fields (e.g. @canonical/anatomy-dsl).
|
|
10
|
+
*
|
|
11
|
+
* @note Impure — reads filesystem.
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
import { existsSync, realpathSync } from "node:fs";
|
|
15
|
+
import { dirname, join } from "node:path";
|
|
16
|
+
import type { PackageRef } from "../../refs/operations/parseRef.js";
|
|
17
|
+
import type { PackageLoader, SemanticPackage } from "../semanticPackage.js";
|
|
18
|
+
import readPackageDir from "./readPackageDir.js";
|
|
19
|
+
|
|
20
|
+
// ---------------------------------------------------------------------------
|
|
21
|
+
// Public API
|
|
22
|
+
// ---------------------------------------------------------------------------
|
|
23
|
+
|
|
24
|
+
export default function createLocalLoader(): PackageLoader {
|
|
25
|
+
return {
|
|
26
|
+
name: "local",
|
|
27
|
+
resolve(ref: PackageRef): SemanticPackage | undefined {
|
|
28
|
+
// file:// refs → direct path check first
|
|
29
|
+
if (ref.kind === "file") {
|
|
30
|
+
if (!existsSync(ref.path)) return undefined;
|
|
31
|
+
const { version, graphs, skills } = readPackageDir(ref.path);
|
|
32
|
+
return { name: ref.pkg, version, source: "local", graphs, skills };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// All ref kinds → try node_modules walk
|
|
36
|
+
const dir = findPackageDir(ref.pkg);
|
|
37
|
+
if (!dir) return undefined;
|
|
38
|
+
|
|
39
|
+
const { version, graphs, skills } = readPackageDir(dir);
|
|
40
|
+
return { name: ref.pkg, version, source: "local", graphs, skills };
|
|
41
|
+
},
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
// ---------------------------------------------------------------------------
|
|
46
|
+
// node_modules walk
|
|
47
|
+
// ---------------------------------------------------------------------------
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Find a package directory by walking up from this module's location.
|
|
51
|
+
*
|
|
52
|
+
* Checks each ancestor's `node_modules/<pkg>` directory. Handles both
|
|
53
|
+
* scoped (`@scope/name`) and unscoped packages. Follows symlinks via
|
|
54
|
+
* `realpathSync` so hoisted workspace layouts resolve correctly.
|
|
55
|
+
*/
|
|
56
|
+
function findPackageDir(pkg: string): string | undefined {
|
|
57
|
+
const segments = pkg.split("/");
|
|
58
|
+
let current = import.meta.dirname;
|
|
59
|
+
|
|
60
|
+
while (current !== dirname(current)) {
|
|
61
|
+
const candidate = join(current, "node_modules", ...segments);
|
|
62
|
+
if (existsSync(candidate)) {
|
|
63
|
+
try {
|
|
64
|
+
return realpathSync(candidate);
|
|
65
|
+
} catch {
|
|
66
|
+
return candidate;
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
current = dirname(current);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return undefined;
|
|
73
|
+
}
|
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Read semantic content from a package directory on disk.
|
|
3
|
+
*
|
|
4
|
+
* Shared by LocalLoader and GitLoader. Given a directory path, reads
|
|
5
|
+
* package.json for version, globs TTL files from definitions/ and data/,
|
|
6
|
+
* and scans skills/ for subdirectories.
|
|
7
|
+
*
|
|
8
|
+
* @note Impure — reads filesystem.
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
import {
|
|
12
|
+
existsSync,
|
|
13
|
+
globSync,
|
|
14
|
+
readdirSync,
|
|
15
|
+
readFileSync,
|
|
16
|
+
statSync,
|
|
17
|
+
} from "node:fs";
|
|
18
|
+
import { join } from "node:path";
|
|
19
|
+
import type { GraphContent, SkillEntry } from "../semanticPackage.js";
|
|
20
|
+
|
|
21
|
+
// ---------------------------------------------------------------------------
|
|
22
|
+
// TTL discovery convention
|
|
23
|
+
// ---------------------------------------------------------------------------
|
|
24
|
+
|
|
25
|
+
const TTL_DIRS: readonly { dir: string; glob: string }[] = [
|
|
26
|
+
{ dir: "definitions", glob: "definitions/**/*.ttl" },
|
|
27
|
+
{ dir: "data", glob: "data/**/*.ttl" },
|
|
28
|
+
];
|
|
29
|
+
|
|
30
|
+
// ---------------------------------------------------------------------------
|
|
31
|
+
// Public API
|
|
32
|
+
// ---------------------------------------------------------------------------
|
|
33
|
+
|
|
34
|
+
export interface PackageDirContents {
|
|
35
|
+
readonly version: string;
|
|
36
|
+
readonly graphs: GraphContent[];
|
|
37
|
+
readonly skills: SkillEntry[];
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* Read semantic package content from a directory.
|
|
42
|
+
*
|
|
43
|
+
* @param dir - Absolute path to the package root directory.
|
|
44
|
+
* @returns Package version, TTL graph content, and skill entries.
|
|
45
|
+
*/
|
|
46
|
+
export default function readPackageDir(dir: string): PackageDirContents {
|
|
47
|
+
return {
|
|
48
|
+
version: readVersion(dir),
|
|
49
|
+
graphs: readGraphs(dir),
|
|
50
|
+
skills: readSkills(dir),
|
|
51
|
+
};
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
// ---------------------------------------------------------------------------
|
|
55
|
+
// Helpers
|
|
56
|
+
// ---------------------------------------------------------------------------
|
|
57
|
+
|
|
58
|
+
function readVersion(dir: string): string {
|
|
59
|
+
const pjsonPath = join(dir, "package.json");
|
|
60
|
+
try {
|
|
61
|
+
const raw = readFileSync(pjsonPath, "utf-8");
|
|
62
|
+
const parsed = JSON.parse(raw) as { version?: string };
|
|
63
|
+
return parsed.version ?? "0.0.0";
|
|
64
|
+
} catch {
|
|
65
|
+
return "0.0.0";
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function readGraphs(dir: string): GraphContent[] {
|
|
70
|
+
const graphs: GraphContent[] = [];
|
|
71
|
+
|
|
72
|
+
for (const entry of TTL_DIRS) {
|
|
73
|
+
if (!existsSync(join(dir, entry.dir))) continue;
|
|
74
|
+
|
|
75
|
+
const files = globSync(entry.glob, { cwd: dir });
|
|
76
|
+
for (const file of files) {
|
|
77
|
+
const absPath = join(dir, file);
|
|
78
|
+
try {
|
|
79
|
+
const content = readFileSync(absPath, "utf-8");
|
|
80
|
+
graphs.push({ path: absPath, content, format: "turtle" });
|
|
81
|
+
} catch {
|
|
82
|
+
// Unreadable file — skip silently
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return graphs;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function readSkills(dir: string): SkillEntry[] {
|
|
91
|
+
const skillsDir = join(dir, "skills");
|
|
92
|
+
if (!existsSync(skillsDir)) return [];
|
|
93
|
+
|
|
94
|
+
const skills: SkillEntry[] = [];
|
|
95
|
+
try {
|
|
96
|
+
const entries = readdirSync(skillsDir);
|
|
97
|
+
for (const entry of entries) {
|
|
98
|
+
const entryPath = join(skillsDir, entry);
|
|
99
|
+
try {
|
|
100
|
+
if (statSync(entryPath).isDirectory()) {
|
|
101
|
+
skills.push({ dir: entryPath, folderName: entry });
|
|
102
|
+
}
|
|
103
|
+
} catch {
|
|
104
|
+
// stat failed — skip
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
} catch {
|
|
108
|
+
// readdir failed — no skills
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
return skills;
|
|
112
|
+
}
|
|
@@ -11,7 +11,12 @@ export default async function lookupMany<TResult>(
|
|
|
11
11
|
queries: readonly string[],
|
|
12
12
|
lookup: (query: string) => Promise<TResult>,
|
|
13
13
|
): Promise<LookupResult<TResult>> {
|
|
14
|
-
const errors: Array<{
|
|
14
|
+
const errors: Array<{
|
|
15
|
+
query: string;
|
|
16
|
+
code: string;
|
|
17
|
+
message: string;
|
|
18
|
+
suggestions?: readonly string[];
|
|
19
|
+
}> = [];
|
|
15
20
|
const settled = await Promise.allSettled(
|
|
16
21
|
queries.map((query) => lookup(query)),
|
|
17
22
|
);
|
|
@@ -35,6 +40,9 @@ export default async function lookupMany<TResult>(
|
|
|
35
40
|
query,
|
|
36
41
|
code: error.code,
|
|
37
42
|
message: error.message,
|
|
43
|
+
...(error.suggestions.length > 0 && {
|
|
44
|
+
suggestions: error.suggestions,
|
|
45
|
+
}),
|
|
38
46
|
});
|
|
39
47
|
} else {
|
|
40
48
|
throw error;
|