@canonical/pragma-cli 0.30.0 → 0.31.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 +64 -35
- package/dist/pragma +0 -0
- package/package.json +15 -13
- package/src/completions/constants.ts +7 -0
- package/src/completions/handleQuery.test.ts +32 -0
- package/src/completions/handleQuery.ts +20 -1
- package/src/completions/queryCompletions.test.ts +5 -0
- package/src/completions/queryCompletions.ts +17 -3
- package/src/completions/querySocket.test.ts +31 -0
- package/src/completions/querySocket.ts +36 -9
- package/src/config/parseConfigValues.ts +2 -0
- package/src/config/readConfigLayers.test.ts +2 -0
- package/src/config/readConfigLayers.ts +3 -0
- package/src/config/types.ts +7 -0
- package/src/constants.ts +3 -1
- package/src/domains/block/commands/sample.ts +2 -1
- package/src/domains/block/formatters/list.test.ts +3 -3
- package/src/domains/block/formatters/lookup.test.ts +5 -5
- package/src/domains/block/orchestration/enrichBlocks.ts +6 -3
- package/src/domains/block/stories.ts +9 -3
- package/src/domains/config/commands/channel.test.ts +16 -0
- package/src/domains/config/commands/channel.ts +14 -1
- package/src/domains/config/commands/framework.test.ts +14 -0
- package/src/domains/config/commands/framework.ts +13 -1
- package/src/domains/config/commands/tier.test.ts +7 -0
- package/src/domains/config/commands/tier.ts +14 -1
- package/src/domains/config/operations/validateTier.test.ts +9 -0
- package/src/domains/config/operations/validateTier.ts +10 -2
- package/src/domains/create/generatorPack/bundled.ts +49 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.test.ts +159 -0
- package/src/domains/create/generatorPack/compileGeneratorPack.ts +312 -0
- package/src/domains/create/index.ts +13 -7
- package/src/domains/create/mcp/specs.ts +6 -161
- package/src/domains/create/renderGeneratorUi.test.ts +103 -0
- package/src/domains/create/renderGeneratorUi.ts +65 -0
- package/src/domains/doctor/formatters/doctor.test.ts +50 -2
- package/src/domains/doctor/formatters/doctor.ts +90 -38
- package/src/domains/doctor/operations/checks/checkMcpCommands.ts +9 -6
- package/src/domains/doctor/operations/checks/checkPackageRefs.ts +30 -12
- package/src/domains/doctor/operations/checks.test.ts +6 -4
- package/src/domains/doctor/operations/types.ts +23 -1
- package/src/domains/graph/commands/inspect.test.ts +2 -2
- package/src/domains/graph/commands/query.test.ts +5 -3
- package/src/domains/graph/formatters/inspect.ts +7 -7
- package/src/domains/graph/formatters/query.test.ts +87 -0
- package/src/domains/graph/formatters/query.ts +42 -5
- package/src/domains/graph/operations/inspectUri.test.ts +5 -5
- package/src/domains/graphql/commands/serve.test.ts +12 -6
- package/src/domains/graphql/commands/serve.ts +16 -6
- package/src/domains/graphql/helpers/parsePrefixes.test.ts +3 -3
- package/src/domains/graphql/helpers/parsePrefixes.ts +3 -3
- package/src/domains/graphql/helpers/resolveSourceFiles.test.ts +9 -0
- package/src/domains/graphql/helpers/resolveSourceFiles.ts +24 -3
- package/src/domains/graphql/operations/compileSchema.test.ts +2 -2
- package/src/domains/graphql/operations/createSchemaServer.test.ts +2 -2
- package/src/domains/graphql/operations/ensureSchemaArtifact.test.ts +167 -0
- package/src/domains/graphql/operations/ensureSchemaArtifact.ts +178 -0
- package/src/domains/graphql/operations/index.ts +5 -0
- package/src/domains/info/formatters/upgrade.test.ts +7 -4
- package/src/domains/info/formatters/upgrade.ts +3 -1
- package/src/domains/info/operations/collectEntityCounts.test.ts +156 -0
- package/src/domains/info/operations/collectEntityCounts.ts +123 -0
- package/src/domains/info/operations/index.ts +1 -0
- package/src/domains/info/types.ts +13 -0
- package/src/domains/llm/capabilities.test.ts +23 -3
- package/src/domains/llm/commands/capabilities.ts +12 -3
- package/src/domains/llm/commands/llm.ts +5 -1
- package/src/domains/llm/data/commandReference.ts +20 -11
- package/src/domains/llm/data/decisionTrees.ts +10 -5
- package/src/domains/llm/data/toolCatalog.ts +49 -18
- package/src/domains/llm/formatters/orientation.ts +5 -1
- package/src/domains/llm/llmCommand.test.ts +11 -3
- package/src/domains/llm/mcp/specs.ts +3 -4
- package/src/domains/llm/operations/collectContext.test.ts +64 -0
- package/src/domains/llm/operations/collectContext.ts +7 -17
- package/src/domains/modifier/commands/sample.ts +2 -1
- package/src/domains/modifier/operations/lookup.ts +5 -3
- package/src/domains/modifier/stories.ts +9 -0
- package/src/domains/ontology/commands/list.test.ts +5 -3
- package/src/domains/ontology/commands/show.test.ts +85 -9
- package/src/domains/ontology/formatters/index.ts +1 -0
- package/src/domains/ontology/formatters/list.ts +37 -12
- package/src/domains/ontology/formatters/show.ts +358 -67
- package/src/domains/ontology/helpers/buildClassTree.ts +89 -0
- package/src/domains/ontology/helpers/completeClassNames.ts +43 -0
- package/src/domains/ontology/helpers/expandOntologyIris.ts +79 -0
- package/src/domains/ontology/helpers/index.ts +14 -0
- package/src/domains/ontology/helpers/queryClasses.ts +32 -16
- package/src/domains/ontology/helpers/queryProperties.ts +39 -6
- package/src/domains/ontology/helpers/queryTboxFacts.ts +160 -0
- package/src/domains/ontology/operations/listOntologies.test.ts +80 -3
- package/src/domains/ontology/operations/listOntologies.ts +126 -30
- package/src/domains/ontology/operations/showOntology.test.ts +131 -12
- package/src/domains/ontology/operations/showOntology.ts +289 -7
- package/src/domains/ontology/operations/showOntologyRaw.test.ts +4 -2
- package/src/domains/ontology/stories.ts +53 -8
- package/src/domains/refs/operations/updateRefs.ts +0 -1
- package/src/domains/setup/commands/index.ts +1 -0
- package/src/domains/setup/commands/setup.ts +70 -0
- package/src/domains/setup/index.ts +7 -3
- package/src/domains/setup/operations/setupLsp.test.ts +3 -2
- package/src/domains/setup/operations/setupLsp.ts +7 -2
- package/src/domains/shared/bootStore.bareCore.test.ts +63 -0
- package/src/domains/shared/bootStore.test.ts +82 -0
- package/src/domains/shared/bootStore.ts +56 -20
- package/src/domains/shared/emptyErrorRecovery.test.ts +23 -26
- package/src/domains/shared/extractLocalName.test.ts +3 -3
- package/src/domains/shared/formatters.ts +23 -2
- package/src/domains/shared/listTiers.ts +40 -0
- package/src/domains/shared/loaders/bundledLoader.ts +48 -0
- package/src/domains/shared/loaders/gitLoader.ts +1 -2
- package/src/domains/shared/loaders/localLoader.ts +2 -18
- package/src/domains/shared/loaders/readPackageDir.test.ts +58 -0
- package/src/domains/shared/loaders/readPackageDir.ts +46 -8
- package/src/domains/shared/lookupMany.test.ts +15 -0
- package/src/domains/shared/lookupMany.ts +22 -6
- package/src/domains/shared/namespaces.ts +13 -5
- package/src/domains/shared/parseSampleCount.test.ts +47 -0
- package/src/domains/shared/parseSampleCount.ts +45 -0
- package/src/domains/shared/prefixes.test.ts +211 -0
- package/src/domains/shared/prefixes.ts +182 -15
- package/src/domains/shared/renderers.ts +5 -5
- package/src/domains/shared/resolveLookupExitCode.test.ts +48 -0
- package/src/domains/shared/resolveLookupExitCode.ts +42 -0
- package/src/domains/shared/semanticPackage.test.ts +89 -0
- package/src/domains/shared/semanticPackage.ts +17 -2
- package/src/domains/shared/stories/compileLookupCommand.ts +3 -1
- package/src/domains/shared/stories/compileLookupTool.ts +1 -1
- package/src/domains/shared/stories/pack/applyPackFilters.test.ts +49 -0
- package/src/domains/shared/stories/pack/applyPackFilters.ts +46 -11
- package/src/domains/shared/stories/pack/applyPackSearch.test.ts +57 -0
- package/src/domains/shared/stories/pack/applyPackSearch.ts +41 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.test.ts +30 -0
- package/src/domains/shared/stories/pack/buildLookupQuery.ts +75 -1
- package/src/domains/shared/stories/pack/bundled/index.ts +23 -0
- package/src/domains/shared/stories/pack/bundled/standardPack.ts +164 -0
- package/src/domains/shared/stories/pack/bundled/tierPack.ts +50 -0
- package/src/domains/shared/stories/pack/collectPackStories.test.ts +305 -7
- package/src/domains/shared/stories/pack/collectPackStories.ts +90 -8
- package/src/domains/shared/stories/pack/compilePackCommands.ts +17 -7
- package/src/domains/shared/stories/pack/compilePackStories.test.ts +607 -2
- package/src/domains/shared/stories/pack/compilePackStories.ts +544 -73
- package/src/domains/shared/stories/pack/compilePackToolSpecs.ts +20 -8
- package/src/domains/shared/stories/pack/index.ts +14 -0
- package/src/domains/shared/stories/pack/reservedVerbs.test.ts +213 -0
- package/src/domains/shared/stories/pack/reservedVerbs.ts +170 -0
- package/src/domains/shared/stories/pack/standardParity.test.ts +293 -220
- package/src/domains/shared/stories/pack/types.ts +123 -3
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.test.ts +502 -0
- package/src/domains/shared/stories/pack/validateStoryPackDefinition.ts +475 -24
- package/src/domains/shared/stories/types.ts +9 -1
- package/src/domains/shared/types/index.ts +5 -9
- package/src/domains/shared/types/ontology.ts +119 -24
- package/src/domains/shared/types/standard.ts +9 -52
- package/src/domains/token/commands/lookup.test.ts +8 -0
- package/src/domains/token/commands/lookup.ts +12 -0
- package/src/domains/token/commands/sample.ts +2 -1
- package/src/domains/token/featureFlag.ts +17 -0
- package/src/domains/token/index.ts +5 -0
- package/src/domains/token/mcp/specs.ts +13 -1
- package/src/domains/trace/commands/resolveTraceLimit.test.ts +31 -0
- package/src/domains/trace/commands/show.ts +30 -4
- package/src/index.ts +1 -13
- package/src/mcp/registerResources.test.ts +2 -2
- package/src/mcp/resources/pickPrimaryType.test.ts +5 -5
- package/src/mcp/tools/index.ts +18 -10
- package/src/mcp/tools/registerTools.test.ts +106 -30
- package/src/pipeline/collectCommands.test.ts +151 -1
- package/src/pipeline/collectCommands.ts +34 -19
- package/src/pipeline/createProgram.test.ts +5 -1
- package/src/pipeline/createProgram.ts +2 -2
- package/src/pipeline/firstRun.test.ts +80 -0
- package/src/pipeline/firstRun.ts +99 -0
- package/src/pipeline/parseGlobalFlags.test.ts +102 -1
- package/src/pipeline/parseGlobalFlags.ts +98 -9
- package/src/pipeline/resolveCommandKind.test.ts +12 -0
- package/src/pipeline/resolveCommandKind.ts +4 -0
- package/src/pipeline/rootHelp.test.ts +87 -0
- package/src/pipeline/rootHelp.ts +265 -0
- package/src/pipeline/runCli.orchestration.test.ts +28 -12
- package/src/pipeline/runCli.test.ts +17 -4
- package/src/pipeline/runCli.ts +162 -19
- package/src/testing/cli.ts +30 -2
- package/src/testing/dsFixtures.ts +21 -4
- package/src/testing/index.ts +1 -4
- package/src/testing/integration/__snapshots__/executor-golden.test.ts.snap +4 -4
- package/src/testing/integration/cli-user-stories.test.ts +63 -0
- package/src/testing/integration/cross-binary-byte-equality.test.ts +2 -2
- package/src/testing/integration/edge-cases.test.ts +62 -50
- package/src/testing/integration/executor-execution.test.ts +6 -4
- package/src/testing/integration/executor-golden.test.ts +2 -2
- package/src/testing/integration/mcp-surface.test.ts +40 -15
- package/src/testing/integration/parity.test.ts +119 -70
- package/src/testing/setupXdgIsolation.ts +16 -5
- package/src/testing/standardParityFixtures.ts +26 -83
- package/src/testing/store.ts +4 -4
- package/src/domains/create/commands/component.test.ts +0 -166
- package/src/domains/create/commands/component.ts +0 -123
- package/src/domains/create/commands/index.ts +0 -3
- package/src/domains/create/commands/package.test.ts +0 -35
- package/src/domains/create/commands/package.ts +0 -21
- package/src/domains/create/generators.ts +0 -8
- package/src/domains/standard/commands/categories.test.ts +0 -70
- package/src/domains/standard/commands/categories.ts +0 -48
- package/src/domains/standard/commands/index.ts +0 -6
- package/src/domains/standard/commands/list.test.ts +0 -105
- package/src/domains/standard/commands/list.ts +0 -11
- package/src/domains/standard/commands/lookup.test.ts +0 -135
- package/src/domains/standard/commands/lookup.ts +0 -14
- package/src/domains/standard/commands/sample.ts +0 -57
- package/src/domains/standard/formatters/categories.test.ts +0 -44
- package/src/domains/standard/formatters/categories.ts +0 -38
- package/src/domains/standard/formatters/index.ts +0 -10
- package/src/domains/standard/formatters/list.test.ts +0 -169
- package/src/domains/standard/formatters/list.ts +0 -184
- package/src/domains/standard/formatters/lookup.test.ts +0 -95
- package/src/domains/standard/formatters/lookup.ts +0 -101
- package/src/domains/standard/formatters/sample.ts +0 -15
- package/src/domains/standard/formatters/types.ts +0 -31
- package/src/domains/standard/index.ts +0 -40
- package/src/domains/standard/mcp/index.ts +0 -2
- package/src/domains/standard/mcp/specs.ts +0 -89
- package/src/domains/standard/operations/categories.test.ts +0 -61
- package/src/domains/standard/operations/categories.ts +0 -38
- package/src/domains/standard/operations/index.ts +0 -6
- package/src/domains/standard/operations/list.test.ts +0 -75
- package/src/domains/standard/operations/list.ts +0 -64
- package/src/domains/standard/operations/lookup.test.ts +0 -58
- package/src/domains/standard/operations/lookup.ts +0 -151
- package/src/domains/standard/operations/sample.ts +0 -35
- package/src/domains/standard/orchestration/buildStandardFilters.ts +0 -11
- package/src/domains/standard/orchestration/index.ts +0 -4
- package/src/domains/standard/orchestration/resolveStandardList.ts +0 -45
- package/src/domains/standard/orchestration/resolveStandardLookup.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.test.ts +0 -33
- package/src/domains/standard/orchestration/standardEmptyError.ts +0 -29
- package/src/domains/standard/standardConfig.ts +0 -42
- package/src/domains/standard/stories.test.ts +0 -24
- package/src/domains/standard/stories.ts +0 -151
- package/src/domains/standard/types.ts +0 -15
- package/src/domains/tier/commands/index.ts +0 -3
- package/src/domains/tier/commands/list.test.ts +0 -67
- package/src/domains/tier/commands/list.ts +0 -9
- package/src/domains/tier/formatters/index.ts +0 -3
- package/src/domains/tier/formatters/list.test.ts +0 -33
- package/src/domains/tier/formatters/list.ts +0 -45
- package/src/domains/tier/index.ts +0 -17
- package/src/domains/tier/mcp/index.ts +0 -2
- package/src/domains/tier/mcp/specs.ts +0 -12
- package/src/domains/tier/operations/index.ts +0 -3
- package/src/domains/tier/operations/list.test.ts +0 -32
- package/src/domains/tier/operations/list.ts +0 -33
- package/src/domains/tier/stories.test.ts +0 -22
- package/src/domains/tier/stories.ts +0 -52
|
@@ -1,8 +1,29 @@
|
|
|
1
1
|
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
2
|
+
import type { PragmaConfig } from "#config";
|
|
2
3
|
import { PragmaError } from "#error";
|
|
3
4
|
import { RECIPE_STORY } from "#testing";
|
|
4
5
|
import type { SemanticPackage } from "../../semanticPackage.js";
|
|
5
|
-
import
|
|
6
|
+
import { BUNDLED_PACKS } from "./bundled/index.js";
|
|
7
|
+
import realCollectPackStories from "./collectPackStories.js";
|
|
8
|
+
import {
|
|
9
|
+
buildReservedVerbs,
|
|
10
|
+
deriveReservedVerbs,
|
|
11
|
+
isReserved,
|
|
12
|
+
type ReservedVerbs,
|
|
13
|
+
} from "./reservedVerbs.js";
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Test wrapper that isolates the always-present bundled packs: unit tests
|
|
17
|
+
* asserting exact entries pass `[]` so the default bundled `tier` pack does not
|
|
18
|
+
* pollute their expectations. Bundled behavior is covered separately below.
|
|
19
|
+
*/
|
|
20
|
+
function collectPackStories(
|
|
21
|
+
config: PragmaConfig,
|
|
22
|
+
packages: readonly SemanticPackage[],
|
|
23
|
+
reserved: ReservedVerbs,
|
|
24
|
+
): ReturnType<typeof realCollectPackStories> {
|
|
25
|
+
return realCollectPackStories(config, packages, reserved, []);
|
|
26
|
+
}
|
|
6
27
|
|
|
7
28
|
function makePackage(stories: SemanticPackage["stories"]): SemanticPackage {
|
|
8
29
|
return {
|
|
@@ -17,6 +38,45 @@ function makePackage(stories: SemanticPackage["stories"]): SemanticPackage {
|
|
|
17
38
|
|
|
18
39
|
const CONFIG = { tier: undefined, channel: "normal" as const };
|
|
19
40
|
|
|
41
|
+
// A representative slice of the real built-in surface: leaf read nouns that
|
|
42
|
+
// own `list`/`lookup`, plus operational nouns (graph, config, create, graphql,
|
|
43
|
+
// setup, tokens) that own neither and so must be reserved wholesale.
|
|
44
|
+
const BUILTIN_PAIRS: readonly (readonly [string, string | undefined])[] = [
|
|
45
|
+
["standard", "list"],
|
|
46
|
+
["standard", "lookup"],
|
|
47
|
+
["standard", "categories"],
|
|
48
|
+
["standard", "sample"],
|
|
49
|
+
["block", "list"],
|
|
50
|
+
["block", "lookup"],
|
|
51
|
+
["token", "list"],
|
|
52
|
+
["token", "lookup"],
|
|
53
|
+
["tier", "list"],
|
|
54
|
+
["config", "show"],
|
|
55
|
+
["config", "tier"],
|
|
56
|
+
["config", "channel"],
|
|
57
|
+
["graph", "query"],
|
|
58
|
+
["graph", "inspect"],
|
|
59
|
+
["create", "component"],
|
|
60
|
+
["create", "package"],
|
|
61
|
+
["graphql", "build"],
|
|
62
|
+
["graphql", "check"],
|
|
63
|
+
["graphql", "serve"],
|
|
64
|
+
["setup", "all"],
|
|
65
|
+
["setup", "lsp"],
|
|
66
|
+
["tokens", "add-config"],
|
|
67
|
+
["info", undefined],
|
|
68
|
+
];
|
|
69
|
+
|
|
70
|
+
/** Run a thunk and return the error it throws (or `undefined`). */
|
|
71
|
+
function caught(run: () => unknown): unknown {
|
|
72
|
+
try {
|
|
73
|
+
run();
|
|
74
|
+
} catch (error) {
|
|
75
|
+
return error;
|
|
76
|
+
}
|
|
77
|
+
return undefined;
|
|
78
|
+
}
|
|
79
|
+
|
|
20
80
|
describe("collectPackStories", () => {
|
|
21
81
|
let warn: ReturnType<typeof vi.spyOn>;
|
|
22
82
|
|
|
@@ -39,7 +99,7 @@ describe("collectPackStories", () => {
|
|
|
39
99
|
},
|
|
40
100
|
]),
|
|
41
101
|
],
|
|
42
|
-
|
|
102
|
+
buildReservedVerbs([["block", undefined]]),
|
|
43
103
|
);
|
|
44
104
|
expect(entries.map((entry) => entry.definition.noun)).toEqual([
|
|
45
105
|
"recipe",
|
|
@@ -57,11 +117,13 @@ describe("collectPackStories", () => {
|
|
|
57
117
|
{ path: "/pkg/stories/recipe.json", definition: RECIPE_STORY },
|
|
58
118
|
]),
|
|
59
119
|
],
|
|
60
|
-
|
|
120
|
+
buildReservedVerbs([]),
|
|
61
121
|
);
|
|
62
122
|
expect(entries).toHaveLength(1);
|
|
63
123
|
expect(entries.at(0)?.source).toBe("config");
|
|
64
|
-
expect(warn).toHaveBeenCalledWith(
|
|
124
|
+
expect(warn).toHaveBeenCalledWith(
|
|
125
|
+
expect.stringContaining("already provided"),
|
|
126
|
+
);
|
|
65
127
|
});
|
|
66
128
|
|
|
67
129
|
it("skips invalid package stories with a warning", () => {
|
|
@@ -72,7 +134,7 @@ describe("collectPackStories", () => {
|
|
|
72
134
|
{ path: "/pkg/stories/broken.json", definition: { nope: true } },
|
|
73
135
|
]),
|
|
74
136
|
],
|
|
75
|
-
|
|
137
|
+
buildReservedVerbs([]),
|
|
76
138
|
);
|
|
77
139
|
expect(entries).toHaveLength(0);
|
|
78
140
|
expect(warn).toHaveBeenCalledWith(
|
|
@@ -85,7 +147,7 @@ describe("collectPackStories", () => {
|
|
|
85
147
|
collectPackStories(
|
|
86
148
|
{ ...CONFIG, stories: [{ ...RECIPE_STORY, noun: "block" }] },
|
|
87
149
|
[],
|
|
88
|
-
|
|
150
|
+
buildReservedVerbs([["block", undefined]]),
|
|
89
151
|
),
|
|
90
152
|
).toThrow(PragmaError);
|
|
91
153
|
});
|
|
@@ -101,8 +163,244 @@ describe("collectPackStories", () => {
|
|
|
101
163
|
},
|
|
102
164
|
]),
|
|
103
165
|
],
|
|
104
|
-
|
|
166
|
+
buildReservedVerbs([["block", undefined]]),
|
|
105
167
|
);
|
|
106
168
|
expect(entries).toHaveLength(0);
|
|
107
169
|
});
|
|
170
|
+
|
|
171
|
+
it("admits a pack once its noun's verbs leave the reserved map", () => {
|
|
172
|
+
// Post-migration simulation: `standard`'s list/lookup wrappers are gone,
|
|
173
|
+
// so only categories/sample remain reserved — a `standard` pack (which
|
|
174
|
+
// emits list + lookup) is now admissible. `info` is a bare (whole-noun)
|
|
175
|
+
// built-in, so an `info` pack stays blocked. This is the keystone: the
|
|
176
|
+
// per-verb guard makes the leaf cutover incremental.
|
|
177
|
+
const reserved = buildReservedVerbs([
|
|
178
|
+
["standard", "categories"],
|
|
179
|
+
["standard", "sample"],
|
|
180
|
+
["info", undefined],
|
|
181
|
+
]);
|
|
182
|
+
|
|
183
|
+
const entries = collectPackStories(
|
|
184
|
+
CONFIG,
|
|
185
|
+
[
|
|
186
|
+
makePackage([
|
|
187
|
+
{
|
|
188
|
+
path: "/pkg/stories/standard.json",
|
|
189
|
+
definition: { ...RECIPE_STORY, noun: "standard" },
|
|
190
|
+
},
|
|
191
|
+
{
|
|
192
|
+
path: "/pkg/stories/info.json",
|
|
193
|
+
definition: { ...RECIPE_STORY, noun: "info" },
|
|
194
|
+
},
|
|
195
|
+
]),
|
|
196
|
+
],
|
|
197
|
+
reserved,
|
|
198
|
+
);
|
|
199
|
+
|
|
200
|
+
expect(entries.map((entry) => entry.definition.noun)).toEqual(["standard"]);
|
|
201
|
+
expect(warn).toHaveBeenCalledWith(
|
|
202
|
+
expect.stringContaining("shadows built-in command"),
|
|
203
|
+
);
|
|
204
|
+
});
|
|
205
|
+
|
|
206
|
+
// Regression restoration (FIX 1): operational nouns own no list/lookup, so
|
|
207
|
+
// deriveReservedVerbs promotes them to a whole-noun reservation. Since a
|
|
208
|
+
// pack always emits `list`, a pack targeting an operational namespace must
|
|
209
|
+
// stay BLOCKED — this is the load-bearing proof that the per-verb flip did
|
|
210
|
+
// not silently open pragma's core operational namespaces to packs.
|
|
211
|
+
describe("operational nouns stay reserved wholesale", () => {
|
|
212
|
+
const reserved = deriveReservedVerbs(BUILTIN_PAIRS);
|
|
213
|
+
|
|
214
|
+
it("promotes every operational noun to a whole-noun reservation", () => {
|
|
215
|
+
for (const noun of ["config", "graph", "create", "graphql", "setup"]) {
|
|
216
|
+
expect(isReserved(reserved, noun, "list")).toBe(true);
|
|
217
|
+
}
|
|
218
|
+
// `tokens` (the plural add-config noun) owns only `add-config`.
|
|
219
|
+
expect(isReserved(reserved, "tokens", "list")).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("throws a config error when a config story shadows an operational noun", () => {
|
|
223
|
+
const error = caught(() =>
|
|
224
|
+
collectPackStories(
|
|
225
|
+
{ ...CONFIG, stories: [{ ...RECIPE_STORY, noun: "graph" }] },
|
|
226
|
+
[],
|
|
227
|
+
reserved,
|
|
228
|
+
),
|
|
229
|
+
);
|
|
230
|
+
expect(error).toBeInstanceOf(PragmaError);
|
|
231
|
+
expect((error as PragmaError).code).toBe("CONFIG_ERROR");
|
|
232
|
+
});
|
|
233
|
+
|
|
234
|
+
it("skips a package story that shadows an operational noun", () => {
|
|
235
|
+
const entries = collectPackStories(
|
|
236
|
+
CONFIG,
|
|
237
|
+
[
|
|
238
|
+
makePackage([
|
|
239
|
+
{
|
|
240
|
+
path: "/pkg/stories/graph.json",
|
|
241
|
+
definition: { ...RECIPE_STORY, noun: "graph" },
|
|
242
|
+
},
|
|
243
|
+
]),
|
|
244
|
+
],
|
|
245
|
+
reserved,
|
|
246
|
+
);
|
|
247
|
+
expect(entries).toHaveLength(0);
|
|
248
|
+
expect(warn).toHaveBeenCalledWith(
|
|
249
|
+
expect.stringContaining("shadows built-in command"),
|
|
250
|
+
);
|
|
251
|
+
});
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
// Keystone incremental window (FIX 3): `standard`'s `list` wrapper is
|
|
255
|
+
// deleted but `lookup` is still built-in (alongside categories/sample).
|
|
256
|
+
describe("incremental leaf cutover window", () => {
|
|
257
|
+
const reserved = deriveReservedVerbs([
|
|
258
|
+
["standard", "lookup"],
|
|
259
|
+
["standard", "categories"],
|
|
260
|
+
["standard", "sample"],
|
|
261
|
+
["info", undefined],
|
|
262
|
+
]);
|
|
263
|
+
|
|
264
|
+
it("keeps `standard` per-verb because it still owns lookup (not promoted)", () => {
|
|
265
|
+
// Disjointness check: the noun retains a read verb, so it must NOT be
|
|
266
|
+
// wholesale-promoted — otherwise `list` could never be freed.
|
|
267
|
+
expect(isReserved(reserved, "standard", "list")).toBe(false);
|
|
268
|
+
expect(isReserved(reserved, "standard", "lookup")).toBe(true);
|
|
269
|
+
});
|
|
270
|
+
|
|
271
|
+
it("admits a standard pack that declares only list", () => {
|
|
272
|
+
const listOnly = { ...RECIPE_STORY, noun: "standard", lookup: undefined };
|
|
273
|
+
const entries = collectPackStories(
|
|
274
|
+
CONFIG,
|
|
275
|
+
[
|
|
276
|
+
makePackage([
|
|
277
|
+
{ path: "/pkg/stories/standard.json", definition: listOnly },
|
|
278
|
+
]),
|
|
279
|
+
],
|
|
280
|
+
reserved,
|
|
281
|
+
);
|
|
282
|
+
expect(entries.map((entry) => entry.definition.noun)).toEqual([
|
|
283
|
+
"standard",
|
|
284
|
+
]);
|
|
285
|
+
});
|
|
286
|
+
|
|
287
|
+
it("rejects a standard pack that also declares lookup (config throws)", () => {
|
|
288
|
+
const error = caught(() =>
|
|
289
|
+
collectPackStories(
|
|
290
|
+
{ ...CONFIG, stories: [{ ...RECIPE_STORY, noun: "standard" }] },
|
|
291
|
+
[],
|
|
292
|
+
reserved,
|
|
293
|
+
),
|
|
294
|
+
);
|
|
295
|
+
expect(error).toBeInstanceOf(PragmaError);
|
|
296
|
+
expect((error as PragmaError).code).toBe("CONFIG_ERROR");
|
|
297
|
+
});
|
|
298
|
+
|
|
299
|
+
it("rejects a standard pack that also declares lookup (package skips)", () => {
|
|
300
|
+
const entries = collectPackStories(
|
|
301
|
+
CONFIG,
|
|
302
|
+
[
|
|
303
|
+
makePackage([
|
|
304
|
+
{
|
|
305
|
+
path: "/pkg/stories/standard.json",
|
|
306
|
+
definition: { ...RECIPE_STORY, noun: "standard" },
|
|
307
|
+
},
|
|
308
|
+
]),
|
|
309
|
+
],
|
|
310
|
+
reserved,
|
|
311
|
+
);
|
|
312
|
+
expect(entries).toHaveLength(0);
|
|
313
|
+
expect(warn).toHaveBeenCalledWith(
|
|
314
|
+
expect.stringContaining("shadows built-in command"),
|
|
315
|
+
);
|
|
316
|
+
});
|
|
317
|
+
|
|
318
|
+
it("rejects a list-only standard pack whose extra verb is still built-in", () => {
|
|
319
|
+
// Pack v1 emissions beyond list/lookup are guarded too: `categories`
|
|
320
|
+
// here collides with the still-registered built-in wrapper.
|
|
321
|
+
const withCategories = {
|
|
322
|
+
...RECIPE_STORY,
|
|
323
|
+
noun: "standard",
|
|
324
|
+
lookup: undefined,
|
|
325
|
+
verbs: [
|
|
326
|
+
{
|
|
327
|
+
verb: "categories",
|
|
328
|
+
query: RECIPE_STORY.list.query,
|
|
329
|
+
columns: RECIPE_STORY.list.columns,
|
|
330
|
+
},
|
|
331
|
+
],
|
|
332
|
+
};
|
|
333
|
+
const error = caught(() =>
|
|
334
|
+
collectPackStories(
|
|
335
|
+
{ ...CONFIG, stories: [withCategories] },
|
|
336
|
+
[],
|
|
337
|
+
reserved,
|
|
338
|
+
),
|
|
339
|
+
);
|
|
340
|
+
expect(error).toBeInstanceOf(PragmaError);
|
|
341
|
+
expect((error as PragmaError).message).toContain("standard categories");
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
it("rejects a pack whose sample capability is still built-in", () => {
|
|
345
|
+
const withSample = {
|
|
346
|
+
...RECIPE_STORY,
|
|
347
|
+
noun: "standard",
|
|
348
|
+
lookup: {
|
|
349
|
+
// biome-ignore lint/style/noNonNullAssertion: fixture always declares lookup
|
|
350
|
+
...RECIPE_STORY.lookup!,
|
|
351
|
+
sample: true as const,
|
|
352
|
+
},
|
|
353
|
+
};
|
|
354
|
+
const error = caught(() =>
|
|
355
|
+
collectPackStories({ ...CONFIG, stories: [withSample] }, [], reserved),
|
|
356
|
+
);
|
|
357
|
+
expect(error).toBeInstanceOf(PragmaError);
|
|
358
|
+
expect((error as PragmaError).message).toContain("standard sample");
|
|
359
|
+
});
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
// Bundled transitional packs use the REAL collect (not the isolating wrapper).
|
|
363
|
+
describe("bundled packs", () => {
|
|
364
|
+
it("includes bundled packs by default, lowest precedence", () => {
|
|
365
|
+
const entries = realCollectPackStories(
|
|
366
|
+
CONFIG,
|
|
367
|
+
[],
|
|
368
|
+
buildReservedVerbs([]),
|
|
369
|
+
);
|
|
370
|
+
const nouns = entries.map((entry) => entry.definition.noun);
|
|
371
|
+
// Every bundled pack is present, tagged with a `bundled:` source.
|
|
372
|
+
for (const pack of BUNDLED_PACKS) {
|
|
373
|
+
expect(nouns).toContain(pack.noun);
|
|
374
|
+
const entry = entries.find((e) => e.definition.noun === pack.noun);
|
|
375
|
+
expect(entry?.source).toBe(`bundled:${pack.noun}`);
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
|
|
379
|
+
it("lets a config story override a bundled noun (config wins)", () => {
|
|
380
|
+
const bundledNoun = BUNDLED_PACKS[0]?.noun ?? "tier";
|
|
381
|
+
const entries = realCollectPackStories(
|
|
382
|
+
{ ...CONFIG, stories: [{ ...RECIPE_STORY, noun: bundledNoun }] },
|
|
383
|
+
[],
|
|
384
|
+
buildReservedVerbs([]),
|
|
385
|
+
);
|
|
386
|
+
const entry = entries.find((e) => e.definition.noun === bundledNoun);
|
|
387
|
+
// The config-declared story wins; the bundled one yields silently.
|
|
388
|
+
expect(entry?.source).toBe("config");
|
|
389
|
+
expect(
|
|
390
|
+
entries.filter((e) => e.definition.noun === bundledNoun),
|
|
391
|
+
).toHaveLength(1);
|
|
392
|
+
});
|
|
393
|
+
|
|
394
|
+
it("yields a bundled noun to a still-reserved built-in", () => {
|
|
395
|
+
// If a bundled noun is still a built-in (not yet cut over), the bundled
|
|
396
|
+
// pack must not shadow it.
|
|
397
|
+
const bundledNoun = BUNDLED_PACKS[0]?.noun ?? "tier";
|
|
398
|
+
const entries = realCollectPackStories(
|
|
399
|
+
CONFIG,
|
|
400
|
+
[],
|
|
401
|
+
buildReservedVerbs([[bundledNoun, undefined]]),
|
|
402
|
+
);
|
|
403
|
+
expect(entries.map((e) => e.definition.noun)).not.toContain(bundledNoun);
|
|
404
|
+
});
|
|
405
|
+
});
|
|
108
406
|
});
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import type { PragmaConfig } from "#config";
|
|
2
2
|
import { PragmaError } from "#error";
|
|
3
3
|
import type { SemanticPackage } from "../../semanticPackage.js";
|
|
4
|
+
import { BUNDLED_PACKS } from "./bundled/index.js";
|
|
5
|
+
import { isReserved, type ReservedVerbs } from "./reservedVerbs.js";
|
|
4
6
|
import type { StoryPackDefinition } from "./types.js";
|
|
5
7
|
import validateStoryPackDefinition from "./validateStoryPackDefinition.js";
|
|
6
8
|
|
|
@@ -10,6 +12,31 @@ export interface PackStoryEntry {
|
|
|
10
12
|
readonly source: string;
|
|
11
13
|
}
|
|
12
14
|
|
|
15
|
+
/**
|
|
16
|
+
* The verbs a pack definition compiles to — mirrors `compilePackStories`,
|
|
17
|
+
* which always emits `list`, adds any declared extra verbs, and adds
|
|
18
|
+
* `lookup`/`sample` only when declared. The reserved-verb guard sees the
|
|
19
|
+
* FULL emission so a pack cannot shadow any built-in verb.
|
|
20
|
+
*/
|
|
21
|
+
function emittedVerbs(definition: StoryPackDefinition): string[] {
|
|
22
|
+
return [
|
|
23
|
+
"list",
|
|
24
|
+
...(definition.verbs ?? []).map((verb) => verb.verb),
|
|
25
|
+
...(definition.lookup ? ["lookup"] : []),
|
|
26
|
+
...(definition.lookup?.sample ? ["sample"] : []),
|
|
27
|
+
];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Name the built-in `(noun, verb)` command(s) a pack collides with, for
|
|
32
|
+
* actionable diagnostics — e.g. `built-in command "standard lookup"`.
|
|
33
|
+
* Since the guard is per-verb, reporting the noun alone is misleading.
|
|
34
|
+
*/
|
|
35
|
+
function describeShadow(noun: string, verbs: readonly string[]): string {
|
|
36
|
+
const pairs = verbs.map((verb) => `"${noun} ${verb}"`).join(", ");
|
|
37
|
+
return `built-in command${verbs.length > 1 ? "s" : ""} ${pairs}`;
|
|
38
|
+
}
|
|
39
|
+
|
|
13
40
|
/**
|
|
14
41
|
* Gather the story-pack definitions active for a runtime.
|
|
15
42
|
*
|
|
@@ -18,28 +45,40 @@ export interface PackStoryEntry {
|
|
|
18
45
|
* config is the user's override layer. Package story files are validated
|
|
19
46
|
* here; an invalid or colliding package story is skipped with a warning
|
|
20
47
|
* so one bad file cannot break boot. A config story colliding with a
|
|
21
|
-
* reserved (built-in) noun is a hard config error.
|
|
48
|
+
* reserved (built-in) noun is a hard config error. Bundled transitional
|
|
49
|
+
* packs come last (lowest precedence), so config or a package can override
|
|
50
|
+
* a bundled noun.
|
|
22
51
|
*
|
|
23
52
|
* @param config - The effective merged configuration.
|
|
24
53
|
* @param packages - Resolved semantic packages (may ship `stories/*.json`).
|
|
25
|
-
* @param
|
|
54
|
+
* @param reserved - Built-in `(noun, verb)` reservations packs must not
|
|
55
|
+
* shadow; a pack collides only when it emits a reserved verb.
|
|
56
|
+
* @param bundled - Transitional packs pragma ships by default (lowest
|
|
57
|
+
* precedence). Defaults to {@link BUNDLED_PACKS}; tests pass `[]` to isolate.
|
|
26
58
|
* @returns Validated, collision-free story entries.
|
|
27
59
|
* @throws PragmaError with code `CONFIG_ERROR` when a config story shadows
|
|
28
|
-
* a reserved
|
|
60
|
+
* a reserved built-in verb.
|
|
29
61
|
* @note Impure — warns on stderr for skipped package stories.
|
|
30
62
|
*/
|
|
31
63
|
export default function collectPackStories(
|
|
32
64
|
config: PragmaConfig,
|
|
33
65
|
packages: readonly SemanticPackage[],
|
|
34
|
-
|
|
66
|
+
reserved: ReservedVerbs,
|
|
67
|
+
bundled: readonly StoryPackDefinition[] = BUNDLED_PACKS,
|
|
35
68
|
): PackStoryEntry[] {
|
|
36
69
|
const entries: PackStoryEntry[] = [];
|
|
37
70
|
const taken = new Set<string>();
|
|
38
71
|
|
|
39
72
|
for (const definition of config.stories ?? []) {
|
|
40
|
-
|
|
73
|
+
const shadowed = emittedVerbs(definition).filter((verb) =>
|
|
74
|
+
isReserved(reserved, definition.noun, verb),
|
|
75
|
+
);
|
|
76
|
+
if (shadowed.length > 0) {
|
|
41
77
|
throw PragmaError.configError(
|
|
42
|
-
`Story noun "${definition.noun}" shadows
|
|
78
|
+
`Story noun "${definition.noun}" shadows ${describeShadow(
|
|
79
|
+
definition.noun,
|
|
80
|
+
shadowed,
|
|
81
|
+
)}.`,
|
|
43
82
|
);
|
|
44
83
|
}
|
|
45
84
|
if (taken.has(definition.noun)) {
|
|
@@ -61,9 +100,21 @@ export default function collectPackStories(
|
|
|
61
100
|
process.stderr.write(`Warning: skipping story — ${reason}\n`);
|
|
62
101
|
continue;
|
|
63
102
|
}
|
|
64
|
-
|
|
103
|
+
const shadowed = emittedVerbs(definition).filter((verb) =>
|
|
104
|
+
isReserved(reserved, definition.noun, verb),
|
|
105
|
+
);
|
|
106
|
+
if (shadowed.length > 0) {
|
|
107
|
+
process.stderr.write(
|
|
108
|
+
`Warning: skipping story "${definition.noun}" from ${file.path} — it shadows ${describeShadow(
|
|
109
|
+
definition.noun,
|
|
110
|
+
shadowed,
|
|
111
|
+
)}.\n`,
|
|
112
|
+
);
|
|
113
|
+
continue;
|
|
114
|
+
}
|
|
115
|
+
if (taken.has(definition.noun)) {
|
|
65
116
|
process.stderr.write(
|
|
66
|
-
`Warning: skipping story "${definition.noun}" from ${file.path} — the noun is already
|
|
117
|
+
`Warning: skipping story "${definition.noun}" from ${file.path} — the noun is already provided by a higher-precedence source.\n`,
|
|
67
118
|
);
|
|
68
119
|
continue;
|
|
69
120
|
}
|
|
@@ -72,5 +123,36 @@ export default function collectPackStories(
|
|
|
72
123
|
}
|
|
73
124
|
}
|
|
74
125
|
|
|
126
|
+
// Bundled transitional packs — lowest precedence, so a config- or
|
|
127
|
+
// package-declared story for the same noun overrides them (the P4 handoff).
|
|
128
|
+
// Authored in-repo but still validated: only a validation failure (an
|
|
129
|
+
// authoring slip) is skipped with a warning (consistent with package
|
|
130
|
+
// stories — one bad pack cannot break boot), and the pack's own parity tests
|
|
131
|
+
// catch a bundled pack that fails to load. When a still-reserved built-in or
|
|
132
|
+
// a higher-precedence source already owns the noun, the bundled pack yields
|
|
133
|
+
// silently (no warning) — the shadow/duplicate check below, matching the
|
|
134
|
+
// precedence order rather than flagging an error.
|
|
135
|
+
for (const raw of bundled) {
|
|
136
|
+
const source = `bundled:${raw.noun}`;
|
|
137
|
+
let definition: StoryPackDefinition;
|
|
138
|
+
try {
|
|
139
|
+
definition = validateStoryPackDefinition(raw, source);
|
|
140
|
+
} catch (error) {
|
|
141
|
+
const reason = error instanceof Error ? error.message : String(error);
|
|
142
|
+
process.stderr.write(`Warning: skipping bundled story — ${reason}\n`);
|
|
143
|
+
continue;
|
|
144
|
+
}
|
|
145
|
+
const shadowed = emittedVerbs(definition).filter((verb) =>
|
|
146
|
+
isReserved(reserved, definition.noun, verb),
|
|
147
|
+
);
|
|
148
|
+
if (shadowed.length > 0 || taken.has(definition.noun)) {
|
|
149
|
+
// A still-registered built-in (not yet cut over) or a higher-precedence
|
|
150
|
+
// source already owns this noun — the bundled pack yields to it silently.
|
|
151
|
+
continue;
|
|
152
|
+
}
|
|
153
|
+
taken.add(definition.noun);
|
|
154
|
+
entries.push({ definition, source });
|
|
155
|
+
}
|
|
156
|
+
|
|
75
157
|
return entries;
|
|
76
158
|
}
|
|
@@ -1,29 +1,31 @@
|
|
|
1
1
|
import type { CommandDefinition } from "@canonical/cli-core";
|
|
2
2
|
import type { PragmaContext } from "../../context.js";
|
|
3
|
-
import {
|
|
3
|
+
import { resolvePrefixes } from "../../prefixes.js";
|
|
4
4
|
import compileLookupCommand from "../compileLookupCommand.js";
|
|
5
5
|
import compileReadCommand from "../compileReadCommand.js";
|
|
6
6
|
import collectPackStories from "./collectPackStories.js";
|
|
7
7
|
import compilePackStories from "./compilePackStories.js";
|
|
8
|
+
import type { ReservedVerbs } from "./reservedVerbs.js";
|
|
8
9
|
|
|
9
10
|
/**
|
|
10
11
|
* Compile every active story pack into CLI command definitions.
|
|
11
12
|
*
|
|
12
13
|
* Story packs come from the merged config (`stories`) and from resolved
|
|
13
14
|
* semantic packages (`stories/*.json`); each yields `<noun> list` and,
|
|
14
|
-
* when declared, `<noun> lookup
|
|
15
|
-
* the built-in read stories, so
|
|
15
|
+
* when declared, `<noun> lookup`, extra list verbs, and `<noun> sample` —
|
|
16
|
+
* projected through the same kernel as the built-in read stories, so
|
|
17
|
+
* completions and help come for free.
|
|
16
18
|
*
|
|
17
19
|
* @param ctx - Pragma context carrying config, packages, and the store.
|
|
18
|
-
* @param
|
|
20
|
+
* @param reserved - Built-in `(noun, verb)` reservations packs must not shadow.
|
|
19
21
|
* @returns Command definitions for every pack story.
|
|
20
22
|
*/
|
|
21
23
|
export default function compilePackCommands(
|
|
22
24
|
ctx: PragmaContext,
|
|
23
|
-
|
|
25
|
+
reserved: ReservedVerbs,
|
|
24
26
|
): CommandDefinition[] {
|
|
25
|
-
const prefixes =
|
|
26
|
-
const entries = collectPackStories(ctx.config, ctx.packages,
|
|
27
|
+
const prefixes = resolvePrefixes(ctx.packages ?? [], ctx.config.prefixes);
|
|
28
|
+
const entries = collectPackStories(ctx.config, ctx.packages ?? [], reserved);
|
|
27
29
|
|
|
28
30
|
return entries.flatMap((entry) => {
|
|
29
31
|
const compiled = compilePackStories(
|
|
@@ -31,10 +33,18 @@ export default function compilePackCommands(
|
|
|
31
33
|
entry.source,
|
|
32
34
|
prefixes,
|
|
33
35
|
);
|
|
36
|
+
// Surface order mirrors the built-in leaf domains: list, lookup,
|
|
37
|
+
// extra verbs, sample.
|
|
34
38
|
const commands = [compileReadCommand(ctx, compiled.list)];
|
|
35
39
|
if (compiled.lookup) {
|
|
36
40
|
commands.push(compileLookupCommand(ctx, compiled.lookup));
|
|
37
41
|
}
|
|
42
|
+
for (const verb of compiled.verbs) {
|
|
43
|
+
commands.push(compileReadCommand(ctx, verb));
|
|
44
|
+
}
|
|
45
|
+
if (compiled.sample) {
|
|
46
|
+
commands.push(compileReadCommand(ctx, compiled.sample));
|
|
47
|
+
}
|
|
38
48
|
return commands;
|
|
39
49
|
});
|
|
40
50
|
}
|