@canonical/pragma-cli 0.33.0 → 0.34.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 +7 -7
- package/dist/pragma +0 -0
- package/package.json +18 -15
- package/pragma.conf.ts +664 -0
- package/src/bin.ts +13 -4
- package/src/capabilities/block.parity.test.ts +250 -0
- package/src/capabilities/capabilities/capabilities.render.ts +3 -2
- package/src/capabilities/capabilities/capabilities.verb.ts +31 -13
- package/src/capabilities/capabilities/catalog.ts +18 -10
- package/src/capabilities/capabilities/hints.ts +13 -19
- package/src/capabilities/colophon/collectColophon.ts +34 -19
- package/src/capabilities/colophon/colophon.verb.ts +8 -4
- package/src/capabilities/colophon/types.ts +16 -7
- package/src/capabilities/config/collectConfigShow.test.ts +57 -0
- package/src/capabilities/config/collectConfigShow.ts +41 -4
- package/src/capabilities/config/fields.ts +0 -27
- package/src/capabilities/config/set.verb.ts +7 -3
- package/src/capabilities/config/show.render.test.ts +77 -5
- package/src/capabilities/config/show.render.ts +7 -12
- package/src/capabilities/config/show.verb.ts +4 -3
- package/src/capabilities/config/types.ts +8 -0
- package/src/capabilities/create/byteEquality.test.ts +78 -100
- package/src/capabilities/create/compiledCreate.subprocess.test.ts +52 -15
- package/src/capabilities/create/constants.ts +75 -0
- package/src/capabilities/create/create.mcp.test.ts +2 -2
- package/src/capabilities/create/create.test.ts +162 -15
- package/src/capabilities/create/create.verb.ts +140 -69
- package/src/capabilities/create/index.ts +2 -10
- package/src/capabilities/create/pickGenerator.ts +34 -28
- package/src/capabilities/create/templates.embedded.generated.ts +4 -29
- package/src/capabilities/create/types.ts +10 -6
- package/src/capabilities/{standard/parity.test.ts → distribution.parity.test.ts} +24 -14
- package/src/capabilities/distribution.test.ts +120 -0
- package/src/capabilities/distribution.ts +59 -0
- package/src/capabilities/doctor/checks/checkConfigFile.ts +6 -5
- package/src/capabilities/doctor/checks/checkKeStore.ts +2 -2
- package/src/capabilities/doctor/checks/checkMcpConfigured.ts +7 -6
- package/src/capabilities/doctor/checks/checkPackageRefs.ts +79 -45
- package/src/capabilities/doctor/checks/checkPragmaVersion.ts +3 -2
- package/src/capabilities/doctor/checks/checkShellCompletions.test.ts +61 -11
- package/src/capabilities/doctor/checks/checkShellCompletions.ts +63 -23
- package/src/capabilities/doctor/checks/checkSkillsSymlinked.ts +3 -2
- package/src/capabilities/doctor/doctor.render.test.ts +6 -6
- package/src/capabilities/doctor/doctor.render.ts +2 -1
- package/src/capabilities/doctor/doctor.test.ts +37 -8
- package/src/capabilities/doctor/doctor.verb.ts +6 -2
- package/src/capabilities/doctor/runChecks.ts +5 -5
- package/src/capabilities/doctor/types.ts +1 -1
- package/src/capabilities/emptyRecovery.test.ts +32 -15
- package/src/capabilities/graph/inspect.verb.ts +3 -2
- package/src/capabilities/graph/query.verb.ts +3 -2
- package/src/capabilities/graph/runQuery.ts +8 -5
- package/src/capabilities/index.ts +22 -18
- package/src/capabilities/info/collectInfo.ts +10 -6
- package/src/capabilities/info/info.render.ts +3 -2
- package/src/capabilities/info/info.verb.ts +3 -2
- package/src/capabilities/lazy.test.ts +93 -2
- package/src/capabilities/ontology/verbs.ts +21 -21
- package/src/capabilities/prompt/prompt.test.ts +12 -6
- package/src/capabilities/prompt/verbs.ts +24 -11
- package/src/capabilities/resources/provider.ts +30 -11
- package/src/capabilities/resources/resources.test.ts +85 -29
- package/src/capabilities/setup/operations/setupGenerator.ts +16 -9
- package/src/capabilities/setup/operations/setupMcp.ts +2 -2
- package/src/capabilities/setup/operations/setupSkills.ts +3 -3
- package/src/capabilities/setup/setup.verb.ts +12 -5
- package/src/capabilities/setup/shell.ts +29 -3
- package/src/capabilities/skill/discover.ts +11 -5
- package/src/capabilities/skill/render.ts +4 -5
- package/src/capabilities/skill/verbs.ts +7 -7
- package/src/capabilities/sources/collectStatus.ts +55 -50
- package/src/capabilities/sources/index.ts +1 -1
- package/src/capabilities/sources/installSkills.ts +2 -2
- package/src/capabilities/sources/runUpdate.ts +113 -122
- package/src/capabilities/sources/sources.test.ts +111 -117
- package/src/capabilities/sources/status.render.test.ts +53 -24
- package/src/capabilities/sources/status.render.ts +24 -29
- package/src/capabilities/sources/status.verb.ts +15 -8
- package/src/capabilities/sources/types.ts +18 -25
- package/src/capabilities/sources/update.render.ts +9 -2
- package/src/capabilities/sources/update.verb.ts +12 -23
- package/src/capabilities/surface.test.ts +33 -10
- package/src/capabilities/tier.parity.test.ts +144 -0
- package/src/capabilities/upgrade/upgrade.verb.ts +7 -3
- package/src/constants.test.ts +8 -6
- package/src/constants.ts +47 -21
- package/src/identity.test.ts +333 -0
- package/src/index.ts +2 -2
- package/src/kernel/completion/__snapshots__/emitScripts.test.ts.snap +5 -2
- package/src/kernel/completion/emitScripts.test.ts +1 -1
- package/src/kernel/completion/emitScripts.ts +4 -2
- package/src/kernel/completion/entitySource.test.ts +185 -41
- package/src/kernel/completion/entitySource.ts +117 -107
- package/src/kernel/completion/model.test.ts +1 -1
- package/src/kernel/completion/model.ts +11 -0
- package/src/kernel/completion/safety.test.ts +82 -36
- package/src/kernel/completion/shellDrive.test.ts +592 -0
- package/src/kernel/completion/templates/fish.ts +28 -6
- package/src/kernel/completion/types.ts +3 -3
- package/src/kernel/config/__snapshots__/firstRun.test.ts.snap +1 -1
- package/src/kernel/config/defaults.test.ts +85 -0
- package/src/kernel/config/defaults.ts +12 -23
- package/src/kernel/config/evaluateProjectConfig.ts +2 -3
- package/src/kernel/config/findProjectConfig.ts +4 -3
- package/src/kernel/config/firstRun.ts +12 -5
- package/src/kernel/config/globalConfig.ts +2 -2
- package/src/kernel/config/index.ts +4 -5
- package/src/kernel/config/paths.ts +17 -17
- package/src/kernel/config/readConfig.test.ts +242 -6
- package/src/kernel/config/readConfig.ts +8 -8
- package/src/kernel/config/schema.test.ts +50 -0
- package/src/kernel/config/schema.ts +97 -10
- package/src/kernel/config/types.ts +73 -19
- package/src/kernel/copy.test.ts +453 -0
- package/src/kernel/error/PragmaError.test.ts +1 -1
- package/src/kernel/error/constants.ts +3 -2
- package/src/kernel/error/index.ts +1 -1
- package/src/kernel/error/recovery.test.ts +22 -45
- package/src/kernel/error/recovery.ts +22 -27
- package/src/kernel/error/types.ts +5 -4
- package/src/kernel/packs/collect.test.ts +195 -11
- package/src/kernel/packs/collect.ts +213 -58
- package/src/kernel/packs/compile.test.ts +163 -7
- package/src/kernel/packs/compile.ts +15 -12
- package/src/kernel/packs/disclosure.test.ts +5 -2
- package/src/kernel/packs/graphql/fetchGraphqlLookup.ts +11 -10
- package/src/kernel/packs/graphqlSource.test.ts +6 -3
- package/src/kernel/packs/iri.ts +5 -7
- package/src/kernel/packs/renderPack.ts +5 -3
- package/src/kernel/packs/resolveEntity.ts +21 -17
- package/src/kernel/packs/runBodies.ts +15 -14
- package/src/kernel/packs/schema.ts +96 -10
- package/src/kernel/packs/sparql/runSelect.test.ts +104 -10
- package/src/kernel/packs/sparql/runSelect.ts +73 -34
- package/src/kernel/packs/toolDescription.test.ts +10 -2
- package/src/kernel/packs/types.ts +39 -4
- package/src/kernel/packs/uniqueness.ts +4 -4
- package/src/kernel/project/cli/buildProgram.test.ts +9 -4
- package/src/kernel/project/cli/dispatch.test.ts +67 -0
- package/src/kernel/project/cli/dispatch.ts +34 -12
- package/src/kernel/project/cli/helpFormat.ts +1 -1
- package/src/kernel/project/cli/rootHelp.ts +105 -105
- package/src/kernel/project/mcp/buildServer.ts +11 -6
- package/src/kernel/project/mcp/instructions.ts +11 -6
- package/src/kernel/project/mcp/parity.test.ts +39 -1
- package/src/kernel/project/mcp/prompts/prompts.test.ts +120 -5
- package/src/kernel/project/mcp/prompts/provider.ts +22 -8
- package/src/kernel/project/mcp/prompts/source.ts +68 -67
- package/src/kernel/project/mcp/registerVerb.ts +25 -6
- package/src/kernel/project/mcp/serve.ts +11 -3
- package/src/kernel/project/mcp/toolDescriptions.test.ts +6 -2
- package/src/kernel/render/disclosure.ts +4 -2
- package/src/kernel/render/prefixes.test.ts +36 -0
- package/src/kernel/render/prefixes.ts +40 -13
- package/src/kernel/render/renderers.test.ts +1 -1
- package/src/kernel/runtime/graphpack/build.ts +29 -3
- package/src/kernel/runtime/graphpack/buildIndex.test.ts +46 -0
- package/src/kernel/runtime/graphpack/buildIndex.ts +52 -17
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +5 -5
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +3 -3
- package/src/kernel/runtime/graphpack/embedded/pack.stories.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +37 -11
- package/src/kernel/runtime/graphpack/graphpack.test.ts +205 -5
- package/src/kernel/runtime/graphpack/hash.ts +3 -2
- package/src/kernel/runtime/graphpack/index.ts +1 -1
- package/src/kernel/runtime/graphpack/manifest.ts +114 -12
- package/src/kernel/runtime/graphpack/read.ts +4 -9
- package/src/kernel/runtime/graphpack/schemas.ts +74 -0
- package/src/kernel/runtime/graphpack/stories.ts +84 -0
- package/src/kernel/runtime/graphpack/types.ts +61 -48
- package/src/kernel/runtime/paths.ts +64 -15
- package/src/kernel/runtime/probe.test.ts +8 -4
- package/src/kernel/runtime/readEntity.ts +6 -5
- package/src/kernel/runtime/refs/gitOps.ts +1 -19
- package/src/kernel/runtime/refs/parseRef.ts +7 -7
- package/src/kernel/runtime/refs/resolve.ts +78 -62
- package/src/kernel/runtime/resolveSources.test.ts +63 -24
- package/src/kernel/runtime/resolveSources.ts +36 -25
- package/src/kernel/runtime/storeReadiness.ts +9 -8
- package/src/kernel/runtime/storeless.test.ts +1 -1
- package/src/kernel/runtime/wasmEmbed.test.ts +22 -19
- package/src/kernel/spec/emitReference.test.ts +3 -3
- package/src/kernel/spec/emitReference.ts +144 -11
- package/src/kernel/spec/emitSurface.ts +9 -5
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/surfaceConformance.ts +8 -0
- package/src/kernel/spec/types.ts +29 -9
- package/src/kernel/spec/validate.ts +2 -1
- package/src/kernel/vocabulary.test.ts +164 -0
- package/src/kernel/vocabulary.ts +146 -0
- package/src/testing/PARITY_GAPS.ts +8 -2
- package/src/testing/behavioral/README.md +3 -3
- package/src/testing/behavioral/completion.test.ts +19 -6
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +4 -3
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +1 -1
- package/src/testing/behavioral/exitCodes.e2e.test.ts +6 -5
- package/src/testing/behavioral/harness.foundation.test.ts +14 -5
- package/src/testing/behavioral/journeys.cli.test.ts +116 -75
- package/src/testing/behavioral/journeys.defaultPack.test.ts +82 -176
- package/src/testing/behavioral/journeys.firstInstall.test.ts +167 -0
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +9 -6
- package/src/testing/behavioral/journeys.packageStories.test.ts +188 -0
- package/src/testing/behavioral/liveReadSurface.ts +4 -3
- package/src/testing/behavioral/mcpSurface.test.ts +9 -4
- package/src/testing/behavioral/staleCommands.test.ts +10 -0
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +7 -7
- package/src/testing/eval/cases/assembly.ts +13 -6
- package/src/testing/eval/cases/effectful.ts +25 -13
- package/src/testing/eval/cases/readNouns.ts +22 -20
- package/src/testing/eval/cases/stable.ts +5 -1
- package/src/testing/eval/eval.test.ts +7 -1
- package/src/testing/fixtures/fixtureCapability.ts +45 -1
- package/src/testing/fixtures/graph/canonical.ts +6 -3
- package/src/testing/fixtures/graph/defaultPack.ts +6 -5
- package/src/testing/fixtures/graph/owlExport.ts +7 -5
- package/src/testing/helpers/fixtureGraph.ts +33 -16
- package/src/testing/helpers/packRuntime.ts +2 -3
- package/src/testing/helpers/projectCli.ts +2 -1
- package/src/testing/helpers/projectMcp.ts +6 -0
- package/src/testing/perf/budgets.test.ts +4 -0
- package/src/testing/perf/budgets.ts +22 -7
- package/src/testing/perf/globalSetup.ts +121 -6
- package/src/types.ts +2 -2
- package/src/capabilities/block/blockList.test.ts +0 -79
- package/src/capabilities/block/blockList.verb.ts +0 -163
- package/src/capabilities/block/index.ts +0 -25
- package/src/capabilities/block/pack.ts +0 -166
- package/src/capabilities/block/parity.test.ts +0 -187
- package/src/capabilities/block/tierChain.ts +0 -78
- package/src/capabilities/colophon/pragmaColophon.ts +0 -66
- package/src/capabilities/modifier/index.ts +0 -15
- package/src/capabilities/modifier/pack.ts +0 -64
- package/src/capabilities/standard/index.ts +0 -18
- package/src/capabilities/standard/pack.ts +0 -128
- package/src/capabilities/tier/index.ts +0 -19
- package/src/capabilities/tier/lookup.render.test.ts +0 -72
- package/src/capabilities/tier/lookup.render.ts +0 -59
- package/src/capabilities/tier/lookup.test.ts +0 -83
- package/src/capabilities/tier/lookup.verb.ts +0 -44
- package/src/capabilities/tier/pack.ts +0 -28
- package/src/capabilities/tier/runLookup.ts +0 -87
- package/src/capabilities/token/addConfig.render.ts +0 -27
- package/src/capabilities/token/addConfig.test.ts +0 -62
- package/src/capabilities/token/addConfig.verb.ts +0 -40
- package/src/capabilities/token/index.ts +0 -19
- package/src/capabilities/token/pack.ts +0 -58
- package/src/capabilities/token/runAddConfig.ts +0 -95
- package/src/kernel/completion/bashDrive.test.ts +0 -73
- package/src/kernel/runtime/graphpack/embedded/sample.ttl +0 -39
- package/src/kernel/runtime/lock.ts +0 -90
|
@@ -1,45 +1,68 @@
|
|
|
1
1
|
import { mkdirSync, mkdtempSync, writeFileSync } from "node:fs";
|
|
2
2
|
import { tmpdir } from "node:os";
|
|
3
|
-
import { join } from "node:path";
|
|
3
|
+
import { dirname, join } from "node:path";
|
|
4
4
|
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
5
|
+
import { readConfig } from "../config/readConfig.js";
|
|
6
|
+
import { embeddedManifest } from "../runtime/graphpack/embedded.js";
|
|
7
|
+
import { activePackPath, packDir } from "../runtime/paths.js";
|
|
8
|
+
import { resolveSources } from "../runtime/resolveSources.js";
|
|
5
9
|
import type { CapabilityModule, VerbSpec } from "../spec/types.js";
|
|
6
10
|
import { runComplete } from "./complete.js";
|
|
7
|
-
import {
|
|
11
|
+
import {
|
|
12
|
+
createIndexEntityReader,
|
|
13
|
+
indexCompletionEnv,
|
|
14
|
+
readPackIndex,
|
|
15
|
+
} from "./entitySource.js";
|
|
8
16
|
|
|
9
|
-
/** A fresh cwd with no
|
|
17
|
+
/** A fresh cwd with no pointer → the reader falls back to the embedded pack. */
|
|
10
18
|
const freshCwd = (): string => mkdtempSync(join(tmpdir(), "pragma-entity-"));
|
|
11
19
|
|
|
12
|
-
/**
|
|
20
|
+
/** The 64-hex content hash the crafted pack is filed under. */
|
|
21
|
+
const CRAFTED_HASH = "1".repeat(64);
|
|
22
|
+
|
|
23
|
+
/** A project pointed at a crafted index.json in a temp pack cache. */
|
|
13
24
|
function projectWithIndex(index: unknown): string {
|
|
14
25
|
const cwd = mkdtempSync(join(tmpdir(), "pragma-idx-cwd-"));
|
|
15
26
|
const cache = mkdtempSync(join(tmpdir(), "pragma-idx-cache-"));
|
|
16
27
|
vi.stubEnv("XDG_CACHE_HOME", cache);
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
);
|
|
21
|
-
const packDir = join(cache, "pragma", "packs", "testhash");
|
|
28
|
+
const pointer = activePackPath(cwd);
|
|
29
|
+
mkdirSync(dirname(pointer), { recursive: true });
|
|
30
|
+
writeFileSync(pointer, CRAFTED_HASH);
|
|
31
|
+
const packDir = join(cache, "pragma", "packs", CRAFTED_HASH);
|
|
22
32
|
mkdirSync(packDir, { recursive: true });
|
|
23
33
|
writeFileSync(join(packDir, "index.json"), JSON.stringify(index));
|
|
24
34
|
return cwd;
|
|
25
35
|
}
|
|
26
36
|
|
|
27
|
-
/**
|
|
37
|
+
/**
|
|
38
|
+
* A crafted index exercising the index / prefixes sources. Deliberately in a
|
|
39
|
+
* NEUTRAL namespace: the reader knows no entity families any more, so a test
|
|
40
|
+
* proving it reads `label` or `altNames` must not need this distribution's
|
|
41
|
+
* vocabulary to say so. Since L-OPEN-9 no live verb declares the `altNames`
|
|
42
|
+
* field (the tier lookup completes entity names like every declared lookup),
|
|
43
|
+
* so this crafted index is where the field's read contract is held.
|
|
44
|
+
*
|
|
45
|
+
* Real-vocabulary coverage lives in two places, and neither is here: the
|
|
46
|
+
* PROTECTED contract describe below reads the shipped index for plain entity
|
|
47
|
+
* NAMES, and `safety.test.ts`'s "every declared name source resolves without
|
|
48
|
+
* constructing the store" drives the live grammar over the shipped index for
|
|
49
|
+
* entity names (`tier lookup ap` → `ds:apps`) and for `prefixes`.
|
|
50
|
+
*/
|
|
28
51
|
const CRAFTED_INDEX = {
|
|
29
52
|
version: 2,
|
|
30
|
-
contentHash:
|
|
31
|
-
prefixes: { ex: "https://example.com/"
|
|
53
|
+
contentHash: CRAFTED_HASH,
|
|
54
|
+
prefixes: { ex: "https://example.com/" },
|
|
32
55
|
instanceCountByType: {},
|
|
33
56
|
entities: [
|
|
34
57
|
{ name: "ex:Button", type: "ex:Component" },
|
|
35
|
-
{ name: "
|
|
58
|
+
{ name: "ex:prompt.build", type: "ex:Prompt", label: "build-a-block" },
|
|
36
59
|
{
|
|
37
|
-
name: "
|
|
38
|
-
type: "
|
|
60
|
+
name: "ex:tier.lxd",
|
|
61
|
+
type: "ex:Tier",
|
|
39
62
|
label: "LXD",
|
|
40
63
|
altNames: ["apps/lxd"],
|
|
41
64
|
},
|
|
42
|
-
{ name: "
|
|
65
|
+
{ name: "ex:tier.core", type: "ex:Tier", label: "core" },
|
|
43
66
|
],
|
|
44
67
|
};
|
|
45
68
|
|
|
@@ -55,15 +78,27 @@ describe("indexCompletionEnv — multi-source names(ref)", () => {
|
|
|
55
78
|
]);
|
|
56
79
|
});
|
|
57
80
|
|
|
58
|
-
it("
|
|
81
|
+
it("index + field label: emits the label, and nothing for an entity without one", async () => {
|
|
59
82
|
const env = indexCompletionEnv(projectWithIndex(CRAFTED_INDEX));
|
|
60
|
-
expect(
|
|
83
|
+
expect(
|
|
84
|
+
await env.names({ from: "index", type: "ex:Prompt", field: "label" }),
|
|
85
|
+
).toEqual(["build-a-block"]);
|
|
86
|
+
// `ex:Button` carries no label. The lookup that declared this field matches
|
|
87
|
+
// on the label, so standing the name in would offer a token that lookup
|
|
88
|
+
// cannot resolve.
|
|
89
|
+
expect(
|
|
90
|
+
await env.names({ from: "index", type: "ex:Component", field: "label" }),
|
|
91
|
+
).toEqual([]);
|
|
61
92
|
});
|
|
62
93
|
|
|
63
|
-
it("
|
|
94
|
+
it("index + field altNames: emits only the alt names, never a label in their place", async () => {
|
|
64
95
|
const env = indexCompletionEnv(projectWithIndex(CRAFTED_INDEX));
|
|
65
|
-
// apps/lxd
|
|
66
|
-
|
|
96
|
+
// `ex:tier.lxd` carries `apps/lxd`; `ex:tier.core` carries none, so its
|
|
97
|
+
// label `core` is NOT offered — a bespoke tier lookup filters on the
|
|
98
|
+
// declared alt-name property and would refuse it.
|
|
99
|
+
expect(
|
|
100
|
+
await env.names({ from: "index", type: "ex:Tier", field: "altNames" }),
|
|
101
|
+
).toEqual(["apps/lxd"]);
|
|
67
102
|
});
|
|
68
103
|
|
|
69
104
|
it("prefixes: the index's prefixes ∪ the default display map", async () => {
|
|
@@ -92,18 +127,23 @@ describe("indexCompletionEnv — multi-source names(ref)", () => {
|
|
|
92
127
|
expect(await env.names({ from: "skills" })).toEqual(["docx"]);
|
|
93
128
|
});
|
|
94
129
|
|
|
95
|
-
it("
|
|
130
|
+
it("a type nothing matches yields [] (prefixes still list the default map)", async () => {
|
|
96
131
|
const env = indexCompletionEnv(
|
|
97
132
|
projectWithIndex({
|
|
98
133
|
version: 2,
|
|
99
|
-
contentHash:
|
|
134
|
+
contentHash: CRAFTED_HASH,
|
|
100
135
|
prefixes: {},
|
|
101
136
|
instanceCountByType: {},
|
|
102
137
|
entities: [{ name: "ex:Button", type: "ex:Component" }],
|
|
103
138
|
}),
|
|
104
139
|
);
|
|
105
|
-
expect(
|
|
106
|
-
|
|
140
|
+
expect(
|
|
141
|
+
await env.names({ from: "index", type: "ex:Prompt", field: "label" }),
|
|
142
|
+
).toEqual([]);
|
|
143
|
+
expect(
|
|
144
|
+
await env.names({ from: "index", type: "ex:Tier", field: "altNames" }),
|
|
145
|
+
).toEqual([]);
|
|
146
|
+
// The display map is compiled in, so it answers with no index at all.
|
|
107
147
|
expect(await env.names({ from: "prefixes" })).toContain("ds");
|
|
108
148
|
});
|
|
109
149
|
});
|
|
@@ -112,16 +152,18 @@ describe("entity source contract (PROTECTED)", () => {
|
|
|
112
152
|
it("reads the embedded index storelessly, filtering by type + partial", () => {
|
|
113
153
|
const read = createIndexEntityReader(freshCwd());
|
|
114
154
|
|
|
115
|
-
//
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
"ex:Dialog",
|
|
120
|
-
]);
|
|
155
|
+
// An abox individual of ds:Component. Membership, not the full 100+ list:
|
|
156
|
+
// the roster moves whenever the design system does, but a design system
|
|
157
|
+
// without a button is a change a human should look at.
|
|
158
|
+
expect(read("ds:Component", "")).toContain("ds:global.component.button");
|
|
121
159
|
// Tbox class.
|
|
122
|
-
expect(read("owl:Class", "")).
|
|
123
|
-
// Partial-prefix filter.
|
|
124
|
-
|
|
160
|
+
expect(read("owl:Class", "")).toContain("ds:Component");
|
|
161
|
+
// Partial-prefix filter: every candidate starts with what was typed.
|
|
162
|
+
const partial = read("ds:Component", "ds:global.component.but");
|
|
163
|
+
expect(partial).toContain("ds:global.component.button");
|
|
164
|
+
for (const name of partial) {
|
|
165
|
+
expect(name.startsWith("ds:global.component.but")).toBe(true);
|
|
166
|
+
}
|
|
125
167
|
// Unknown type → no matches (never throws).
|
|
126
168
|
expect(read("ds:Nope", "")).toEqual([]);
|
|
127
169
|
});
|
|
@@ -129,16 +171,16 @@ describe("entity source contract (PROTECTED)", () => {
|
|
|
129
171
|
it("relies only on the frozen { name, type } minimum", () => {
|
|
130
172
|
const read = createIndexEntityReader(freshCwd());
|
|
131
173
|
// Every result is a bare name token (string), usable with no other field.
|
|
132
|
-
for (const name of read("
|
|
174
|
+
for (const name of read("ds:Component", "")) {
|
|
133
175
|
expect(typeof name).toBe("string");
|
|
134
|
-
expect(name.startsWith("
|
|
176
|
+
expect(name.startsWith("ds:")).toBe(true);
|
|
135
177
|
}
|
|
136
178
|
});
|
|
137
179
|
|
|
138
180
|
it("is fast (well under the 50ms storeless budget)", () => {
|
|
139
181
|
const read = createIndexEntityReader(freshCwd());
|
|
140
182
|
const start = performance.now();
|
|
141
|
-
read("
|
|
183
|
+
read("ds:Component", "");
|
|
142
184
|
expect(performance.now() - start).toBeLessThan(50);
|
|
143
185
|
});
|
|
144
186
|
});
|
|
@@ -159,7 +201,7 @@ describe("__complete entity tier wiring", () => {
|
|
|
159
201
|
required: true,
|
|
160
202
|
complete: {
|
|
161
203
|
kind: "names",
|
|
162
|
-
source: { from: "index", type: "
|
|
204
|
+
source: { from: "index", type: "ds:Component" },
|
|
163
205
|
},
|
|
164
206
|
},
|
|
165
207
|
],
|
|
@@ -185,14 +227,116 @@ describe("__complete entity tier wiring", () => {
|
|
|
185
227
|
// index-backed reader over cwd, adapted to the resolver's EntityNameReader.
|
|
186
228
|
await expect(
|
|
187
229
|
runComplete(
|
|
188
|
-
["block", "lookup", "
|
|
230
|
+
["block", "lookup", "ds:global.component.but"],
|
|
189
231
|
[lookupModule],
|
|
190
232
|
indexCompletionEnv(freshCwd()),
|
|
191
233
|
),
|
|
192
|
-
).resolves.
|
|
234
|
+
).resolves.toContain("ds:global.component.button");
|
|
193
235
|
// Without the env, the entity tier yields nothing (grammar-only).
|
|
194
236
|
await expect(
|
|
195
|
-
runComplete(
|
|
237
|
+
runComplete(
|
|
238
|
+
["block", "lookup", "ds:global.component.but"],
|
|
239
|
+
[lookupModule],
|
|
240
|
+
),
|
|
196
241
|
).resolves.toEqual([]);
|
|
197
242
|
});
|
|
198
243
|
});
|
|
244
|
+
|
|
245
|
+
describe("readPackIndex answers the boot decision, never around it", () => {
|
|
246
|
+
afterEach(() => {
|
|
247
|
+
vi.unstubAllEnvs();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
// `info`, `doctor`, the MCP resource browser and native `prompts/list` all
|
|
251
|
+
// read through this. It takes the decision rather than re-deriving one, so it
|
|
252
|
+
// covers all three arms — including the one a re-derivation gets wrong.
|
|
253
|
+
it("reads the pack the decision names", () => {
|
|
254
|
+
projectWithIndex(CRAFTED_INDEX); // plants the pack under a stubbed cache
|
|
255
|
+
const index = readPackIndex({
|
|
256
|
+
kind: "pack",
|
|
257
|
+
dir: packDir(CRAFTED_HASH),
|
|
258
|
+
contentHash: CRAFTED_HASH,
|
|
259
|
+
});
|
|
260
|
+
expect(index?.contentHash).toBe(CRAFTED_HASH);
|
|
261
|
+
expect(index?.entities.map((entity) => entity.name)).toContain("ex:Button");
|
|
262
|
+
});
|
|
263
|
+
|
|
264
|
+
it("reads the embedded snapshot on the embedded arm", () => {
|
|
265
|
+
const index = readPackIndex({ kind: "embedded" });
|
|
266
|
+
expect(index?.contentHash).toBe(embeddedManifest().contentHash);
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
it("reads NOTHING when the store is unavailable", () => {
|
|
270
|
+
// The row that matters: a project that declared its own packs and never
|
|
271
|
+
// built them must not be listed the distribution's graph while every read
|
|
272
|
+
// of it fails STORE_UNAVAILABLE.
|
|
273
|
+
expect(readPackIndex({ kind: "unavailable", reason: "not built" })).toBe(
|
|
274
|
+
undefined,
|
|
275
|
+
);
|
|
276
|
+
});
|
|
277
|
+
});
|
|
278
|
+
|
|
279
|
+
describe("the storeless fast path implements the pointer half", () => {
|
|
280
|
+
afterEach(() => {
|
|
281
|
+
vi.unstubAllEnvs();
|
|
282
|
+
});
|
|
283
|
+
|
|
284
|
+
// `__complete` cannot see `origins.packs` (the config evaluator is banned on
|
|
285
|
+
// this graph), so it cannot distinguish a fresh install from a configured-but-
|
|
286
|
+
// unbuilt project. It CAN see the pointer, and must not prefer the snapshot
|
|
287
|
+
// over a pointer whose pack the cache lost — that is the decision table's
|
|
288
|
+
// second row, where the two readers used to disagree.
|
|
289
|
+
it("offers no candidates when the pointed-at pack is gone", async () => {
|
|
290
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-idx-cwd-"));
|
|
291
|
+
const cache = mkdtempSync(join(tmpdir(), "pragma-idx-cache-"));
|
|
292
|
+
vi.stubEnv("XDG_CACHE_HOME", cache);
|
|
293
|
+
const pointer = activePackPath(cwd);
|
|
294
|
+
mkdirSync(dirname(pointer), { recursive: true });
|
|
295
|
+
writeFileSync(pointer, CRAFTED_HASH); // …but no pack directory for it.
|
|
296
|
+
|
|
297
|
+
expect(createIndexEntityReader(cwd)("ds:Component", "")).toEqual([]);
|
|
298
|
+
expect(
|
|
299
|
+
await indexCompletionEnv(cwd).names({ from: "index", type: "" }),
|
|
300
|
+
).toEqual([]);
|
|
301
|
+
});
|
|
302
|
+
|
|
303
|
+
it("still offers the snapshot's names in a configured-but-unbuilt project (the documented price)", async () => {
|
|
304
|
+
// The bounded exception `entitySource.ts` documents, pinned as it IS and
|
|
305
|
+
// not as it should be. The fast path is deliberately denied the config
|
|
306
|
+
// layer, so a `pragma.config.ts` declaring packs is invisible to it: with
|
|
307
|
+
// no pointer it cannot tell this project from a fresh install, and it
|
|
308
|
+
// answers from the embedded snapshot.
|
|
309
|
+
//
|
|
310
|
+
// Both halves are asserted against the SAME cwd, because the ASYMMETRY is
|
|
311
|
+
// the whole claim — asserting only that the snapshot answers would restate
|
|
312
|
+
// the fresh-install case the PROTECTED contract describe already covers,
|
|
313
|
+
// and would pass with the config file deleted. The read half refusing is
|
|
314
|
+
// what makes this directory a configured-but-unbuilt project rather than a
|
|
315
|
+
// fresh install. (`doctor` says so too, in the same words: verified against
|
|
316
|
+
// the compiled binary, where `block list` raises STORE_UNAVAILABLE while
|
|
317
|
+
// this same completion still answers.)
|
|
318
|
+
//
|
|
319
|
+
// If a later change gives the fast path a config-free way to see
|
|
320
|
+
// `origins.packs`, THIS is the test that must move, deliberately.
|
|
321
|
+
const cwd = mkdtempSync(join(tmpdir(), "pragma-unbuilt-cwd-"));
|
|
322
|
+
vi.stubEnv(
|
|
323
|
+
"XDG_CACHE_HOME",
|
|
324
|
+
mkdtempSync(join(tmpdir(), "pragma-unbuilt-")),
|
|
325
|
+
);
|
|
326
|
+
writeFileSync(
|
|
327
|
+
join(cwd, "pragma.config.ts"),
|
|
328
|
+
'export default { packs: [{ name: "unbuilt", source: "file:///pragma-never-built" }] };\n',
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
const decision = resolveSources(await readConfig(cwd), cwd);
|
|
332
|
+
expect(decision).toEqual({
|
|
333
|
+
kind: "unavailable",
|
|
334
|
+
reason: "packs are configured but the store has not been built",
|
|
335
|
+
});
|
|
336
|
+
expect(readPackIndex(decision)).toBeUndefined();
|
|
337
|
+
|
|
338
|
+
expect(createIndexEntityReader(cwd)("ds:Component", "")).toContain(
|
|
339
|
+
"ds:global.component.button",
|
|
340
|
+
);
|
|
341
|
+
});
|
|
342
|
+
});
|
|
@@ -4,46 +4,59 @@
|
|
|
4
4
|
* browser (`info`, `doctor`, MCP prompts, the resource provider) shares.
|
|
5
5
|
*
|
|
6
6
|
* {@link indexCompletionEnv} is the one source-dispatched reader wired at the
|
|
7
|
-
* `__complete` fast path (bin) and the `__complete` verb. It resolves
|
|
7
|
+
* `__complete` fast path (bin) and the `__complete` verb. It resolves three
|
|
8
8
|
* storeless sources — all disk-readable, never a store/SPARQL:
|
|
9
|
-
* - `index` → the active pack's `index.json
|
|
9
|
+
* - `index` → the active pack's `index.json`, filtered by a prefixed `type`
|
|
10
|
+
* and read from the declared `field` (`name` by default).
|
|
10
11
|
* - `skills` → SKILL.md names via {@link discoverSkills} (filesystem walk).
|
|
11
|
-
* - `prompts` → the pack index's `ds:Prompt` entities (`label || name`).
|
|
12
|
-
* - `tiers` → the pack index's `ds:Tier` entities (their `ds:name`, else
|
|
13
|
-
* `label ?? name` when the index carries no `ds:name`).
|
|
14
12
|
* - `prefixes` → the pack index's prefixes ∪ the default display prefixes.
|
|
15
13
|
*
|
|
14
|
+
* There is no per-family source. An entity family IS a `type` filter plus the
|
|
15
|
+
* field that family is addressed by, and both are declared at the verb, so this
|
|
16
|
+
* module names no entity type at all.
|
|
17
|
+
*
|
|
16
18
|
* The index is loaded once (lazy `readFileSync` + plain `JSON.parse` — never a
|
|
17
19
|
* store, facade, config evaluator, or zod schema; any I/O or parse error
|
|
18
|
-
* degrades to `[]`) and shared across the index
|
|
20
|
+
* degrades to `[]`) and shared across the index and prefixes sources;
|
|
19
21
|
* the skills walk is memoized to one walk per `__complete` process.
|
|
20
22
|
* {@link createIndexEntityReader} exposes the index read for the storeless
|
|
21
23
|
* behavioural/safety tests; {@link emptyNameSource} is the default (no names).
|
|
22
24
|
*
|
|
23
25
|
* {@link readPackIndex} and {@link entityTotal} expose the same storeless read
|
|
24
|
-
* to `info`/`doctor` and the resource surfaces
|
|
25
|
-
* the
|
|
26
|
-
*
|
|
26
|
+
* to `info`/`doctor` and the resource surfaces — but those callers are NOT on
|
|
27
|
+
* the fast path, so `readPackIndex` takes the boot decision rather than
|
|
28
|
+
* re-deriving one. Everything here relies only on the FROZEN `{ name, type }`
|
|
29
|
+
* index minimum (plus optional enrichment like `label`/`altNames`), so the
|
|
30
|
+
* index can grow without breaking completion.
|
|
27
31
|
*
|
|
28
32
|
* Storeless-graph notes:
|
|
29
33
|
* - The embedded fallback is read from `pack.index.generated` — its OWN
|
|
30
34
|
* generated module (only the index string) — so the storeless `__complete`
|
|
31
|
-
* path never
|
|
32
|
-
* `pack.generated`.
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
*
|
|
36
|
-
*
|
|
37
|
-
*
|
|
38
|
-
*
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
35
|
+
* path never EVALUATES the n-quads/schema/manifest strings that live in
|
|
36
|
+
* `pack.generated`. It does not avoid parsing them: `bun build --compile`
|
|
37
|
+
* emits one script, so the whole embed is parsed at process start on every
|
|
38
|
+
* invocation (~+25 ms here, measured on `--version` against a toy-embed
|
|
39
|
+
* binary; BUDGETS.md records it). What the split buys is that a completion
|
|
40
|
+
* never allocates the 1.87 MB.
|
|
41
|
+
* - The active pack is resolved through `kernel/runtime/paths` — a LEAF module
|
|
42
|
+
* (node builtins only) that shares the pointer read with `resolveSources`.
|
|
43
|
+
* `resolveSources` itself is unreachable from here: it pulls the graphpack
|
|
44
|
+
* manifest schema (zod), which `safety.test.ts` forbids on this graph, and
|
|
45
|
+
* `origins.packs` needs the config evaluator. So {@link loadActiveIndex}
|
|
46
|
+
* implements only the POINTER half of the decision table — pointer → that
|
|
47
|
+
* pack (else nothing), no pointer → the embedded snapshot. It cannot tell a
|
|
48
|
+
* fresh install from a project that declared its own packs and never built
|
|
49
|
+
* them, so a `__complete` in the latter still offers the snapshot's names.
|
|
50
|
+
* That is the price of a config-free fast path, and it is bounded to
|
|
51
|
+
* completion candidates: every read, and every surface that takes the
|
|
52
|
+
* decision, refuses. `PackIndex`/`PackIndexEntity` are imported type-only, so
|
|
53
|
+
* no zod schema is loaded at runtime.
|
|
54
|
+
* - `discoverSkills` (`node:fs/os/path` only) and `DEFAULT_PREFIX_MAP` (whose
|
|
55
|
+
* domain half the distribution declares) are the only new edges — both
|
|
56
|
+
* leaf-clean, so the fast path stays free of boot/config/store/zod.
|
|
43
57
|
*/
|
|
44
58
|
|
|
45
|
-
import {
|
|
46
|
-
import { homedir } from "node:os";
|
|
59
|
+
import { readFileSync } from "node:fs";
|
|
47
60
|
import { join } from "node:path";
|
|
48
61
|
import { discoverSkills } from "../../capabilities/skill/discover.js";
|
|
49
62
|
// Inlined embedded index — its OWN generated module (only the index string), so
|
|
@@ -52,49 +65,41 @@ import { discoverSkills } from "../../capabilities/skill/discover.js";
|
|
|
52
65
|
import { DEFAULT_PREFIX_MAP } from "../render/prefixes.js";
|
|
53
66
|
import { indexJson as EMBEDDED_INDEX_JSON } from "../runtime/graphpack/embedded/pack.index.generated.js";
|
|
54
67
|
import type { PackIndex, PackIndexEntity } from "../runtime/graphpack/types.js";
|
|
55
|
-
import
|
|
68
|
+
import { packDir, readActivePack } from "../runtime/paths.js";
|
|
69
|
+
import type { SourcesDecision } from "../runtime/resolveSources.js";
|
|
70
|
+
import type { CompletionField, CompletionSourceRef } from "../spec/types.js";
|
|
56
71
|
import type { CompletionEnv } from "./types.js";
|
|
57
72
|
|
|
58
|
-
/** The prefixed KG type every prompt entity carries (inlined; no cycle). */
|
|
59
|
-
const PROMPT_TYPE = "ds:Prompt";
|
|
60
|
-
/** The prefixed KG type every tier entity carries (inlined; no cycle). */
|
|
61
|
-
const TIER_TYPE = "ds:Tier";
|
|
62
|
-
|
|
63
73
|
/** The default name source: no index tier, so no candidates. */
|
|
64
74
|
export const emptyNameSource: CompletionEnv["names"] = () => [];
|
|
65
75
|
|
|
66
|
-
/** The committed project lock basename (mirrors `kernel/runtime/paths`). */
|
|
67
|
-
const LOCK_BASENAME = "pragma.lock.json";
|
|
68
76
|
/** The pack index filename (kept local so this path never imports the zod schema). */
|
|
69
77
|
const INDEX_FILE = "index.json";
|
|
70
78
|
|
|
71
79
|
/**
|
|
72
|
-
*
|
|
80
|
+
* Read the storeless index of the pack the BOOT DECISION names — for `info`,
|
|
81
|
+
* `doctor`, the MCP resource browser, and native `prompts/list`. Never boots the
|
|
82
|
+
* store, never validates with zod — a plain `JSON.parse` off disk.
|
|
73
83
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
const base = process.env.XDG_CACHE_HOME ?? join(homedir(), ".cache");
|
|
79
|
-
return join(base, "pragma", "packs", contentHash);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
/** The committed project lock file for a working directory. */
|
|
83
|
-
function lockPath(cwd: string): string {
|
|
84
|
-
return join(cwd, LOCK_BASENAME);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
/**
|
|
88
|
-
* Read the active pack's storeless index (the locked pack, else the embedded
|
|
89
|
-
* fallback), for the resource browser's list/autocomplete. Never boots the
|
|
90
|
-
* store, never validates with zod — a plain `JSON.parse` off disk. Returns
|
|
91
|
-
* `undefined` when no index is reachable, so callers degrade to a recovery hint.
|
|
84
|
+
* It takes the decision rather than re-deriving one so these surfaces cannot
|
|
85
|
+
* list entities the same project's reads refuse: `unavailable` yields
|
|
86
|
+
* `undefined`, and callers degrade to a recovery hint (`buildResourceList`
|
|
87
|
+
* emits the `pragma:sources` entry, `info` omits the total).
|
|
92
88
|
*
|
|
93
|
-
* @param
|
|
94
|
-
* @returns The
|
|
89
|
+
* @param decision - The boot decision from `resolveSources`.
|
|
90
|
+
* @returns The answering pack's index, or `undefined` when none answers.
|
|
95
91
|
*/
|
|
96
|
-
export function readPackIndex(
|
|
97
|
-
|
|
92
|
+
export function readPackIndex(
|
|
93
|
+
decision: SourcesDecision,
|
|
94
|
+
): PackIndex | undefined {
|
|
95
|
+
switch (decision.kind) {
|
|
96
|
+
case "pack":
|
|
97
|
+
return readIndexFile(decision.dir);
|
|
98
|
+
case "embedded":
|
|
99
|
+
return parseIndex(EMBEDDED_INDEX_JSON);
|
|
100
|
+
case "unavailable":
|
|
101
|
+
return undefined;
|
|
102
|
+
}
|
|
98
103
|
}
|
|
99
104
|
|
|
100
105
|
/**
|
|
@@ -120,31 +125,37 @@ export function entityTotal(index: PackIndex): number {
|
|
|
120
125
|
return subjects.size;
|
|
121
126
|
}
|
|
122
127
|
|
|
123
|
-
/**
|
|
124
|
-
function
|
|
128
|
+
/** Parse an index JSON string; any malformed input degrades to `undefined`. */
|
|
129
|
+
function parseIndex(json: string): PackIndex | undefined {
|
|
125
130
|
try {
|
|
126
|
-
|
|
127
|
-
if (existsSync(path)) {
|
|
128
|
-
const lock = JSON.parse(readFileSync(path, "utf-8")) as {
|
|
129
|
-
contentHash?: unknown;
|
|
130
|
-
};
|
|
131
|
-
if (typeof lock.contentHash === "string") {
|
|
132
|
-
const indexPath = join(packDir(lock.contentHash), INDEX_FILE);
|
|
133
|
-
if (existsSync(indexPath)) {
|
|
134
|
-
return JSON.parse(readFileSync(indexPath, "utf-8")) as PackIndex;
|
|
135
|
-
}
|
|
136
|
-
}
|
|
137
|
-
}
|
|
131
|
+
return JSON.parse(json) as PackIndex;
|
|
138
132
|
} catch {
|
|
139
|
-
|
|
133
|
+
return undefined;
|
|
140
134
|
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/** Read a pack directory's `index.json`; any I/O or parse error is `undefined`. */
|
|
138
|
+
function readIndexFile(dir: string): PackIndex | undefined {
|
|
141
139
|
try {
|
|
142
|
-
return
|
|
140
|
+
return parseIndex(readFileSync(join(dir, INDEX_FILE), "utf-8"));
|
|
143
141
|
} catch {
|
|
144
142
|
return undefined;
|
|
145
143
|
}
|
|
146
144
|
}
|
|
147
145
|
|
|
146
|
+
/**
|
|
147
|
+
* The fast path's index — the POINTER half of the decision table (see the module
|
|
148
|
+
* docblock). A project with a pointer reads that pack and nothing else: a
|
|
149
|
+
* pointer whose pack the cache lost yields no candidates rather than the
|
|
150
|
+
* snapshot's, which is what the boot decision does with the same state.
|
|
151
|
+
*/
|
|
152
|
+
function loadActiveIndex(cwd: string): PackIndex | undefined {
|
|
153
|
+
const contentHash = readActivePack(cwd);
|
|
154
|
+
return contentHash === undefined
|
|
155
|
+
? parseIndex(EMBEDDED_INDEX_JSON)
|
|
156
|
+
: readIndexFile(packDir(contentHash));
|
|
157
|
+
}
|
|
158
|
+
|
|
148
159
|
/** Whether an entity matches a prefixed type filter (primary type or any type). */
|
|
149
160
|
function matchesType(entity: PackIndexEntity, type: string): boolean {
|
|
150
161
|
if (!type) return true;
|
|
@@ -180,42 +191,45 @@ export function createIndexEntityReader(
|
|
|
180
191
|
};
|
|
181
192
|
}
|
|
182
193
|
|
|
183
|
-
/**
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
194
|
+
/**
|
|
195
|
+
* One entity's completable tokens for a declared field, with NO fallback.
|
|
196
|
+
*
|
|
197
|
+
* A verb declares the field because that is the one its lookup matches on, so
|
|
198
|
+
* an entity the pack did not enrich contributes nothing. Standing a label in
|
|
199
|
+
* for an absent alt name would offer a token the lookup provably cannot resolve
|
|
200
|
+
* — a completion that hands the user a value and then refuses it — which is the
|
|
201
|
+
* complete-but-cannot-resolve split this whole declaration exists to close.
|
|
202
|
+
* Offering nothing for an under-enriched entity is the honest answer.
|
|
203
|
+
*
|
|
204
|
+
* @param entity - One index entity.
|
|
205
|
+
* @param field - The declared field to read.
|
|
206
|
+
* @returns The tokens that entity contributes; empty when it carries none.
|
|
207
|
+
*/
|
|
208
|
+
function readEntityNames(
|
|
209
|
+
entity: PackIndexEntity,
|
|
210
|
+
field: CompletionField,
|
|
211
|
+
): readonly string[] {
|
|
212
|
+
switch (field) {
|
|
213
|
+
case "name":
|
|
214
|
+
return [entity.name];
|
|
215
|
+
case "label":
|
|
216
|
+
return entity.label ? [entity.label] : [];
|
|
217
|
+
case "altNames":
|
|
218
|
+
return entity.altNames ?? [];
|
|
200
219
|
}
|
|
201
|
-
return [...names].sort();
|
|
202
220
|
}
|
|
203
221
|
|
|
204
|
-
/**
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
*/
|
|
210
|
-
function tierNames(index: PackIndex | undefined): string[] {
|
|
222
|
+
/** A ref's candidates: entities of its type, read from its field, sorted. */
|
|
223
|
+
function indexNames(
|
|
224
|
+
index: PackIndex | undefined,
|
|
225
|
+
ref: CompletionSourceRef,
|
|
226
|
+
): string[] {
|
|
211
227
|
if (!index) return [];
|
|
212
228
|
const names = new Set<string>();
|
|
213
229
|
for (const entity of index.entities) {
|
|
214
|
-
if (!matchesType(entity,
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
} else {
|
|
218
|
-
names.add(entity.label || entity.name);
|
|
230
|
+
if (!matchesType(entity, ref.type ?? "")) continue;
|
|
231
|
+
for (const name of readEntityNames(entity, ref.field ?? "name")) {
|
|
232
|
+
names.add(name);
|
|
219
233
|
}
|
|
220
234
|
}
|
|
221
235
|
return [...names].sort();
|
|
@@ -235,8 +249,8 @@ function prefixNames(index: PackIndex | undefined): string[] {
|
|
|
235
249
|
* reader every name family shares. Wired at the `__complete` fast path (bin) and
|
|
236
250
|
* the `__complete` verb; storeless throughout (see the module docblock).
|
|
237
251
|
*
|
|
238
|
-
* The pack index is loaded once, lazily, and reused across the index
|
|
239
|
-
*
|
|
252
|
+
* The pack index is loaded once, lazily, and reused across the index and
|
|
253
|
+
* prefixes sources; the skills filesystem walk is memoized to one walk per
|
|
240
254
|
* process (the design's perf caveat). Every source returns the FULL candidate
|
|
241
255
|
* list in canonical casing — the resolver ranks/filters against the partial.
|
|
242
256
|
*
|
|
@@ -262,13 +276,9 @@ export function indexCompletionEnv(cwd: string): CompletionEnv {
|
|
|
262
276
|
names(ref: CompletionSourceRef): readonly string[] {
|
|
263
277
|
switch (ref.from) {
|
|
264
278
|
case "index":
|
|
265
|
-
return indexNames(getIndex(), ref
|
|
279
|
+
return indexNames(getIndex(), ref);
|
|
266
280
|
case "skills":
|
|
267
281
|
return getSkills();
|
|
268
|
-
case "prompts":
|
|
269
|
-
return promptNames(getIndex());
|
|
270
|
-
case "tiers":
|
|
271
|
-
return tierNames(getIndex());
|
|
272
282
|
case "prefixes":
|
|
273
283
|
return prefixNames(getIndex());
|
|
274
284
|
}
|