@canonical/pragma-cli 0.36.0 → 0.38.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 +3 -2
- package/bundled-skills/adoption-a1-styles/SKILL.md +24 -0
- package/bundled-skills/adoption-a2-components/SKILL.md +22 -6
- package/bundled-skills/anatomy-author/ANATOMY_DSL_SPEC.md +1397 -327
- package/bundled-skills/anatomy-author/SKILL.md +840 -137
- package/bundled-skills/design-auditor/SKILL.md +133 -9
- package/bundled-skills/implement-component/SKILL.md +192 -23
- package/bundled-skills/specify-component/SKILL.md +124 -12
- package/bundled-skills/specify-pattern/SKILL.md +47 -8
- package/dist/package.json +23 -20
- package/dist/pragma.conf.js +963 -40
- package/dist/pragma.conf.js.map +1 -1
- package/dist/src/capabilities/capabilities/catalog.js +14 -5
- package/dist/src/capabilities/capabilities/catalog.js.map +1 -1
- package/dist/src/capabilities/capabilities/hints.js +33 -5
- package/dist/src/capabilities/capabilities/hints.js.map +1 -1
- package/dist/src/capabilities/config/show.render.js +30 -2
- package/dist/src/capabilities/config/show.render.js.map +1 -1
- package/dist/src/capabilities/create/create.verb.js +2 -1
- package/dist/src/capabilities/create/create.verb.js.map +1 -1
- package/dist/src/capabilities/create/createSurface.generated.js +23 -1
- package/dist/src/capabilities/create/createSurface.generated.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js +11 -6
- package/dist/src/capabilities/doctor/checks/checkNodeVersion.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js +1 -1
- package/dist/src/capabilities/doctor/checks/harnessInventory.js.map +1 -1
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js +38 -0
- package/dist/src/capabilities/doctor/checks/isSupportedNodeVersion.js.map +1 -0
- package/dist/src/capabilities/doctor/checks/targetHealth.js +104 -14
- package/dist/src/capabilities/doctor/checks/targetHealth.js.map +1 -1
- package/dist/src/capabilities/graph/connect.render.js +101 -0
- package/dist/src/capabilities/graph/connect.render.js.map +1 -0
- package/dist/src/capabilities/graph/connect.types.js +21 -0
- package/dist/src/capabilities/graph/connect.types.js.map +1 -0
- package/dist/src/capabilities/graph/connect.verb.js +147 -0
- package/dist/src/capabilities/graph/connect.verb.js.map +1 -0
- package/dist/src/capabilities/graph/index.js +5 -3
- package/dist/src/capabilities/graph/index.js.map +1 -1
- package/dist/src/capabilities/graph/queryText.js +206 -0
- package/dist/src/capabilities/graph/queryText.js.map +1 -0
- package/dist/src/capabilities/graph/relationIndex.js +302 -0
- package/dist/src/capabilities/graph/relationIndex.js.map +1 -0
- package/dist/src/capabilities/graph/runConnect.js +456 -0
- package/dist/src/capabilities/graph/runConnect.js.map +1 -0
- package/dist/src/capabilities/graph/runQuery.js +21 -4
- package/dist/src/capabilities/graph/runQuery.js.map +1 -1
- package/dist/src/capabilities/setup/buildPlan.js +6 -2
- package/dist/src/capabilities/setup/buildPlan.js.map +1 -1
- package/dist/src/capabilities/setup/operations/index.js +7 -2
- package/dist/src/capabilities/setup/operations/index.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupCompletions.js +6 -2
- package/dist/src/capabilities/setup/operations/setupCompletions.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupConfig.js +4 -2
- package/dist/src/capabilities/setup/operations/setupConfig.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupGenerator.js +72 -14
- package/dist/src/capabilities/setup/operations/setupGenerator.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupLsp.js +372 -62
- package/dist/src/capabilities/setup/operations/setupLsp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupMcp.js +150 -5
- package/dist/src/capabilities/setup/operations/setupMcp.js.map +1 -1
- package/dist/src/capabilities/setup/operations/setupSkills.js +4 -2
- package/dist/src/capabilities/setup/operations/setupSkills.js.map +1 -1
- package/dist/src/capabilities/setup/operations/writability.js +114 -0
- package/dist/src/capabilities/setup/operations/writability.js.map +1 -0
- package/dist/src/capabilities/setup/plan.js.map +1 -1
- package/dist/src/capabilities/setup/setup.verb.js +80 -31
- package/dist/src/capabilities/setup/setup.verb.js.map +1 -1
- package/dist/src/capabilities/setup/targets.js +124 -42
- package/dist/src/capabilities/setup/targets.js.map +1 -1
- package/dist/src/capabilities/shared/index.js +1 -1
- package/dist/src/capabilities/shared/index.js.map +1 -1
- package/dist/src/constants.js +7 -1
- package/dist/src/constants.js.map +1 -1
- package/dist/src/kernel/error/renderError.js +3 -2
- package/dist/src/kernel/error/renderError.js.map +1 -1
- package/dist/src/kernel/error/validOptions.js +41 -0
- package/dist/src/kernel/error/validOptions.js.map +1 -0
- package/dist/src/kernel/packs/compile.js +90 -6
- package/dist/src/kernel/packs/compile.js.map +1 -1
- package/dist/src/kernel/packs/cursor.js +135 -0
- package/dist/src/kernel/packs/cursor.js.map +1 -0
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js +9 -2
- package/dist/src/kernel/packs/graphql/buildLookupDocument.js.map +1 -1
- package/dist/src/kernel/packs/paging.js +108 -0
- package/dist/src/kernel/packs/paging.js.map +1 -0
- package/dist/src/kernel/packs/renderPack.js +154 -5
- package/dist/src/kernel/packs/renderPack.js.map +1 -1
- package/dist/src/kernel/packs/resolveEntity.js +118 -19
- package/dist/src/kernel/packs/resolveEntity.js.map +1 -1
- package/dist/src/kernel/packs/runBodies.js +156 -18
- package/dist/src/kernel/packs/runBodies.js.map +1 -1
- package/dist/src/kernel/packs/schema.js +160 -12
- package/dist/src/kernel/packs/schema.js.map +1 -1
- package/dist/src/kernel/packs/sparql/authorQuery.js +227 -0
- package/dist/src/kernel/packs/sparql/authorQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js +251 -0
- package/dist/src/kernel/packs/sparql/buildListQuery.js.map +1 -0
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js +125 -22
- package/dist/src/kernel/packs/sparql/buildLookupQuery.js.map +1 -1
- package/dist/src/kernel/packs/sparql/filterValues.js +145 -0
- package/dist/src/kernel/packs/sparql/filterValues.js.map +1 -0
- package/dist/src/kernel/packs/sparql/index.js +24 -14
- package/dist/src/kernel/packs/sparql/index.js.map +1 -1
- package/dist/src/kernel/packs/sparql/runSelect.js +10 -4
- package/dist/src/kernel/packs/sparql/runSelect.js.map +1 -1
- package/dist/src/kernel/packs/sparql/searchTerm.js +25 -0
- package/dist/src/kernel/packs/sparql/searchTerm.js.map +1 -0
- package/dist/src/kernel/packs/storyRules.js +151 -0
- package/dist/src/kernel/packs/storyRules.js.map +1 -0
- package/dist/src/kernel/packs/tierScope.js +310 -0
- package/dist/src/kernel/packs/tierScope.js.map +1 -0
- package/dist/src/kernel/packs/types.js +80 -0
- package/dist/src/kernel/packs/types.js.map +1 -1
- package/dist/src/kernel/project/cli/buildProgram.js +10 -1
- package/dist/src/kernel/project/cli/buildProgram.js.map +1 -1
- package/dist/src/kernel/project/cli/dispatch.js +104 -3
- package/dist/src/kernel/project/cli/dispatch.js.map +1 -1
- package/dist/src/kernel/project/cli/rootHelp.js +8 -5
- package/dist/src/kernel/project/cli/rootHelp.js.map +1 -1
- package/dist/src/kernel/project/cli/suggestNames.js +31 -7
- package/dist/src/kernel/project/cli/suggestNames.js.map +1 -1
- package/dist/src/kernel/project/mcp/registerVerb.js +5 -1
- package/dist/src/kernel/project/mcp/registerVerb.js.map +1 -1
- package/dist/src/kernel/render/renderers.js +34 -2
- package/dist/src/kernel/render/renderers.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js +2 -3
- package/dist/src/kernel/runtime/graphpack/embedded/pack.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.index.generated.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js +4 -0
- package/dist/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.js.map +1 -0
- package/dist/src/kernel/runtime/graphpack/embedded.js +13 -6
- package/dist/src/kernel/runtime/graphpack/embedded.js.map +1 -1
- package/dist/src/kernel/runtime/graphpack/read.js +17 -10
- package/dist/src/kernel/runtime/graphpack/read.js.map +1 -1
- package/dist/src/kernel/runtime/loadSession.js +3 -1
- package/dist/src/kernel/runtime/loadSession.js.map +1 -1
- package/dist/src/kernel/runtime/probe.js +1 -1
- package/dist/src/kernel/runtime/probe.js.map +1 -1
- package/dist/src/kernel/runtime/readEntity.js +19 -4
- package/dist/src/kernel/runtime/readEntity.js.map +1 -1
- package/dist/src/kernel/runtime/refs/parseRef.js +17 -1
- package/dist/src/kernel/runtime/refs/parseRef.js.map +1 -1
- package/dist/src/kernel/runtime/refs/resolve.js +38 -5
- package/dist/src/kernel/runtime/refs/resolve.js.map +1 -1
- package/dist/src/kernel/spec/emitReference.js +1 -1
- package/dist/src/kernel/spec/emitReference.js.map +1 -1
- package/package.json +23 -20
- package/pragma.conf.ts +983 -40
- package/src/capabilities/block.parity.test.ts +207 -27
- package/src/capabilities/block.shipped.exec.test.ts +156 -14
- package/src/capabilities/block.tierRank.exec.test.ts +46 -9
- package/src/capabilities/capabilities/catalog.ts +14 -5
- package/src/capabilities/capabilities/hints.ts +40 -5
- package/src/capabilities/config/collectConfigShow.test.ts +1 -0
- package/src/capabilities/config/set.test.ts +24 -25
- package/src/capabilities/config/show.render.test.ts +9 -4
- package/src/capabilities/config/show.render.ts +30 -2
- package/src/capabilities/create/byteEquality.test.ts +28 -1
- package/src/capabilities/create/create.test.ts +1 -0
- package/src/capabilities/create/create.verb.ts +2 -1
- package/src/capabilities/create/createGrammar.test.ts +4 -1
- package/src/capabilities/create/createSurface.generated.ts +23 -1
- package/src/capabilities/create/crossCli.subprocess.test.ts +10 -3
- package/src/capabilities/distribution.parity.test.ts +12 -11
- package/src/capabilities/distribution.test.ts +77 -7
- package/src/capabilities/doctor/checks/checkNodeVersion.test.ts +49 -0
- package/src/capabilities/doctor/checks/checkNodeVersion.ts +11 -7
- package/src/capabilities/doctor/checks/harnessInventory.test.ts +13 -6
- package/src/capabilities/doctor/checks/harnessInventory.ts +8 -4
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.test.ts +111 -0
- package/src/capabilities/doctor/checks/isSupportedNodeVersion.ts +34 -0
- package/src/capabilities/doctor/checks/mcpCommand.test.ts +7 -1
- package/src/capabilities/doctor/checks/targetHealth.ts +114 -13
- package/src/capabilities/doctor/doctor.test.ts +327 -36
- package/src/capabilities/emptyRecovery.test.ts +84 -28
- package/src/capabilities/graph/connect.render.ts +141 -0
- package/src/capabilities/graph/connect.shipped.exec.test.ts +370 -0
- package/src/capabilities/graph/connect.test.ts +361 -0
- package/src/capabilities/graph/connect.types.ts +136 -0
- package/src/capabilities/graph/connect.verb.ts +165 -0
- package/src/capabilities/graph/index.ts +5 -3
- package/src/capabilities/graph/query.shipped.exec.test.ts +66 -0
- package/src/capabilities/graph/query.test.ts +17 -0
- package/src/capabilities/graph/queryText.test.ts +121 -0
- package/src/capabilities/graph/queryText.ts +240 -0
- package/src/capabilities/graph/relationIndex.ts +372 -0
- package/src/capabilities/graph/runConnect.ts +577 -0
- package/src/capabilities/graph/runQuery.ts +21 -6
- package/src/capabilities/lazy.test.ts +32 -2
- package/src/capabilities/listBudget.shipped.exec.test.ts +239 -0
- package/src/capabilities/listLookup.shipped.exec.test.ts +387 -0
- package/src/capabilities/listQuery.shipped.exec.test.ts +400 -0
- package/src/capabilities/resources/resources.test.ts +38 -2
- package/src/capabilities/setup/buildPlan.ts +7 -1
- package/src/capabilities/setup/operations/index.ts +14 -0
- package/src/capabilities/setup/operations/setupCompletions.ts +9 -2
- package/src/capabilities/setup/operations/setupConfig.ts +7 -2
- package/src/capabilities/setup/operations/setupGenerator.ts +93 -15
- package/src/capabilities/setup/operations/setupLsp.ts +544 -85
- package/src/capabilities/setup/operations/setupMcp.ts +212 -4
- package/src/capabilities/setup/operations/setupSkills.ts +7 -2
- package/src/capabilities/setup/operations/writability.test.ts +151 -0
- package/src/capabilities/setup/operations/writability.ts +135 -0
- package/src/capabilities/setup/plan.ts +11 -0
- package/src/capabilities/setup/setup.test.ts +992 -16
- package/src/capabilities/setup/setup.verb.ts +84 -36
- package/src/capabilities/setup/targets.ts +152 -48
- package/src/capabilities/setup/types.ts +41 -5
- package/src/capabilities/shared/index.ts +1 -0
- package/src/capabilities/sources/sources.test.ts +55 -0
- package/src/capabilities/surface.test.ts +59 -11
- package/src/capabilities/tier.parity.test.ts +2 -2
- package/src/capabilities/token.parity.test.ts +784 -0
- package/src/capabilities/tokenGraph.cli.test.ts +279 -0
- package/src/capabilities/tokenGraph.shipped.exec.test.ts +653 -0
- package/src/capabilities/variable.parity.test.ts +448 -0
- package/src/constants.ts +8 -0
- package/src/kernel/completion/shellDrive.test.ts +32 -30
- package/src/kernel/config/defaults.test.ts +21 -2
- package/src/kernel/config/readConfig.test.ts +1 -1
- package/src/kernel/error/renderError.ts +3 -2
- package/src/kernel/error/validOptions.test.ts +57 -0
- package/src/kernel/error/validOptions.ts +45 -0
- package/src/kernel/packs/collect.test.ts +2 -0
- package/src/kernel/packs/compile.test.ts +188 -15
- package/src/kernel/packs/compile.ts +109 -14
- package/src/kernel/packs/cursor.ts +135 -0
- package/src/kernel/packs/filteredEmpty.test.ts +213 -0
- package/src/kernel/packs/graphql/buildLookupDocument.ts +16 -9
- package/src/kernel/packs/graphqlSource.test.ts +152 -0
- package/src/kernel/packs/listNotice.test.ts +156 -0
- package/src/kernel/packs/paging.ts +110 -0
- package/src/kernel/packs/renderPack.ts +186 -13
- package/src/kernel/packs/resolveEntity.test.ts +62 -3
- package/src/kernel/packs/resolveEntity.ts +186 -26
- package/src/kernel/packs/runBodies.ts +196 -26
- package/src/kernel/packs/schema.ts +180 -13
- package/src/kernel/packs/sparql/authorQuery.test.ts +179 -0
- package/src/kernel/packs/sparql/authorQuery.ts +257 -0
- package/src/kernel/packs/sparql/buildListQuery.test.ts +344 -0
- package/src/kernel/packs/sparql/buildListQuery.ts +344 -0
- package/src/kernel/packs/sparql/buildLookupQuery.test.ts +138 -0
- package/src/kernel/packs/sparql/buildLookupQuery.ts +140 -20
- package/src/kernel/packs/sparql/filterValues.test.ts +86 -0
- package/src/kernel/packs/sparql/filterValues.ts +182 -0
- package/src/kernel/packs/sparql/index.ts +30 -14
- package/src/kernel/packs/sparql/runSelect.test.ts +63 -0
- package/src/kernel/packs/sparql/runSelect.ts +42 -2
- package/src/kernel/packs/sparql/searchTerm.ts +29 -0
- package/src/kernel/packs/storyRules.ts +180 -0
- package/src/kernel/packs/tierScope.test.ts +487 -0
- package/src/kernel/packs/tierScope.ts +409 -0
- package/src/kernel/packs/toolDescription.test.ts +13 -10
- package/src/kernel/packs/types.ts +311 -9
- package/src/kernel/project/cli/buildProgram.ts +17 -1
- package/src/kernel/project/cli/dispatch.test.ts +87 -0
- package/src/kernel/project/cli/dispatch.ts +114 -4
- package/src/kernel/project/cli/rootHelp.ts +8 -5
- package/src/kernel/project/cli/suggest.test.ts +35 -0
- package/src/kernel/project/cli/suggestNames.ts +34 -7
- package/src/kernel/project/mcp/registerVerb.ts +5 -1
- package/src/kernel/render/contracts.ts +2 -0
- package/src/kernel/render/prefixes.test.ts +2 -0
- package/src/kernel/render/renderers.test.ts +43 -0
- package/src/kernel/render/renderers.ts +34 -1
- package/src/kernel/runtime/graphpack/embedded/pack.generated.ts +2 -3
- package/src/kernel/runtime/graphpack/embedded/pack.index.generated.ts +1 -1
- package/src/kernel/runtime/graphpack/embedded/pack.manifest.generated.ts +3 -0
- package/src/kernel/runtime/graphpack/embedded.ts +14 -6
- package/src/kernel/runtime/graphpack/graphpack.test.ts +31 -5
- package/src/kernel/runtime/graphpack/read.ts +17 -13
- package/src/kernel/runtime/loadSession.ts +3 -1
- package/src/kernel/runtime/probe.ts +1 -1
- package/src/kernel/runtime/readEntity.ts +19 -4
- package/src/kernel/runtime/refs/parseRef.test.ts +49 -1
- package/src/kernel/runtime/refs/parseRef.ts +25 -1
- package/src/kernel/runtime/refs/resolve.ts +39 -5
- package/src/kernel/runtime/types.ts +30 -0
- package/src/kernel/spec/emitReference.ts +1 -1
- package/src/kernel/spec/surfaceConformance.test.ts +2 -2
- package/src/kernel/spec/types.ts +17 -0
- package/src/testing/PARITY_GAPS.ts +4 -4
- package/src/testing/behavioral/agentSession.mcp.test.ts +43 -39
- package/src/testing/behavioral/completion.test.ts +7 -6
- package/src/testing/behavioral/disclosure.test.ts +39 -37
- package/src/testing/behavioral/errorMatrix.mcp.test.ts +82 -64
- package/src/testing/behavioral/errorMatrix.storeless.test.ts +11 -10
- package/src/testing/behavioral/journeys.cli.test.ts +71 -32
- package/src/testing/behavioral/journeys.defaultPack.test.ts +6 -4
- package/src/testing/behavioral/journeys.firstInstall.test.ts +70 -5
- package/src/testing/behavioral/journeys.mcpLifecycle.mcp.test.ts +4 -3
- package/src/testing/behavioral/journeys.packageStories.test.ts +2 -1
- package/src/testing/behavioral/mcpSurface.test.ts +28 -25
- package/src/testing/behavioral/parity.test.ts +25 -23
- package/src/testing/eval/__snapshots__/eval.test.ts.snap +20 -0
- package/src/testing/eval/cases/effectful.ts +7 -1
- package/src/testing/eval/cases/readNouns.ts +100 -11
- package/src/testing/fixtures/blockGraph.ts +89 -5
- package/src/testing/fixtures/connectGraph.ts +159 -0
- package/src/testing/fixtures/graph/canonical.ts +128 -32
- package/src/testing/helpers/fsProbe.ts +88 -0
- package/src/testing/helpers/runCli.test.ts +44 -0
- package/src/testing/helpers/runCli.ts +27 -3
- package/src/testing/perf/budgets.test.ts +3 -1
- package/src/testing/perf/budgets.ts +138 -54
- package/dist/src/kernel/packs/sparql/applyFilters.js +0 -167
- package/dist/src/kernel/packs/sparql/applyFilters.js.map +0 -1
- package/dist/src/kernel/packs/sparql/applySearch.js +0 -25
- package/dist/src/kernel/packs/sparql/applySearch.js.map +0 -1
- package/src/kernel/packs/sparql/applyFilters.ts +0 -203
- package/src/kernel/packs/sparql/applySearch.ts +0 -33
|
@@ -0,0 +1,361 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graph connect` over a fixture graph authored for it.
|
|
3
|
+
*
|
|
4
|
+
* The five negative answers and the traversal policy are tested HERE, against
|
|
5
|
+
* data this repo owns, because that is the only place exact numbers mean
|
|
6
|
+
* anything: the shipped pack's hop counts and path sets will move the moment new
|
|
7
|
+
* symbols and binding records land, so pinning them there would fail on a
|
|
8
|
+
* correct change. What the shipped pack is tested for is invariants — see
|
|
9
|
+
* `connect.shipped.exec.test.ts`.
|
|
10
|
+
*
|
|
11
|
+
* Each of the five causes is reached, and each is asserted to be
|
|
12
|
+
* DISTINGUISHABLE from the other four: two refusals with different codes, and
|
|
13
|
+
* three answers with different discriminators. That is the whole point of
|
|
14
|
+
* `connect`'s output contract — a caller who cannot tell "I have never heard of
|
|
15
|
+
* this name" from "these two are unrelated" has been told nothing.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
19
|
+
import { verbKey } from "../../kernel/packs/uniqueness.js";
|
|
20
|
+
import { ROSTER_THRESHOLD } from "../../kernel/runtime/readEntity.js";
|
|
21
|
+
import type { PragmaRuntime } from "../../kernel/runtime/types.js";
|
|
22
|
+
import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
23
|
+
import {
|
|
24
|
+
CHAIN_LENGTH,
|
|
25
|
+
CONNECT_PREFIXES,
|
|
26
|
+
CONNECT_TTL,
|
|
27
|
+
HUB_ROSTER_SIZE,
|
|
28
|
+
ROSTER_SIZE,
|
|
29
|
+
} from "../../testing/fixtures/connectGraph.js";
|
|
30
|
+
import { buildFixtureRuntime } from "../../testing/helpers/packRuntime.js";
|
|
31
|
+
import type { ConnectResult } from "./connect.types.js";
|
|
32
|
+
import { graphModule } from "./index.js";
|
|
33
|
+
|
|
34
|
+
const DS = "https://ds.canonical.com/";
|
|
35
|
+
const RDF_TYPE = "http://www.w3.org/1999/02/22-rdf-syntax-ns#type";
|
|
36
|
+
|
|
37
|
+
const connect = graphModule.verbs.find(
|
|
38
|
+
(v) => verbKey(v.path) === "graph connect",
|
|
39
|
+
) as VerbSpec;
|
|
40
|
+
|
|
41
|
+
let rt: PragmaRuntime;
|
|
42
|
+
beforeAll(async () => {
|
|
43
|
+
({ rt } = await buildFixtureRuntime({
|
|
44
|
+
ttl: CONNECT_TTL,
|
|
45
|
+
prefixes: CONNECT_PREFIXES,
|
|
46
|
+
}));
|
|
47
|
+
});
|
|
48
|
+
afterAll(async () => {
|
|
49
|
+
(await rt.store.get()).store.dispose();
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
/** Ask one connect question through the verb, as a projector would. */
|
|
53
|
+
async function ask(
|
|
54
|
+
a: string,
|
|
55
|
+
b: string,
|
|
56
|
+
options: { hops?: number; paths?: number } = {},
|
|
57
|
+
): Promise<ConnectResult> {
|
|
58
|
+
return (await connect.run({ a, b, ...options }, rt)) as ConnectResult;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/** The error a question raises, or `undefined` when it answered. */
|
|
62
|
+
async function refusal(
|
|
63
|
+
a: string,
|
|
64
|
+
b: string,
|
|
65
|
+
options: { hops?: number; paths?: number } = {},
|
|
66
|
+
): Promise<{ code?: string; message: string; suggestions?: string[] }> {
|
|
67
|
+
try {
|
|
68
|
+
await ask(a, b, options);
|
|
69
|
+
} catch (error) {
|
|
70
|
+
return error as { code?: string; message: string; suggestions?: string[] };
|
|
71
|
+
}
|
|
72
|
+
throw new Error(`connect ${a} ${b} answered where it should have refused`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
describe("graph connect — the traversal policy", () => {
|
|
76
|
+
it("walks a chain and reports the hop count and the limit it used", async () => {
|
|
77
|
+
const result = await ask("ds:a1", "ds:a5");
|
|
78
|
+
expect(result.connected).toBe(true);
|
|
79
|
+
expect(result.hops).toBe(4);
|
|
80
|
+
expect(result.pathCount).toBe(1);
|
|
81
|
+
expect(result.paths[0]?.steps).toHaveLength(4);
|
|
82
|
+
expect(result.hopLimit).toBe(4);
|
|
83
|
+
expect(result.rosterThreshold).toBe(ROSTER_THRESHOLD);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("walks each step in the direction the triple is asserted", async () => {
|
|
87
|
+
// ds:r2 points at BOTH ends, so the only route runs against the first
|
|
88
|
+
// edge's direction. A path that dropped `direction` would claim
|
|
89
|
+
// `ds:r1 ds:pointsAt ds:r2`, which the graph does not assert.
|
|
90
|
+
const result = await ask("ds:r1", "ds:r3");
|
|
91
|
+
expect(result.connected).toBe(true);
|
|
92
|
+
expect(result.paths[0]?.steps.map((step) => step.direction)).toEqual([
|
|
93
|
+
"reverse",
|
|
94
|
+
"forward",
|
|
95
|
+
]);
|
|
96
|
+
expect(result.paths[0]?.steps.map((step) => step.to.value)).toEqual([
|
|
97
|
+
`${DS}r2`,
|
|
98
|
+
`${DS}r3`,
|
|
99
|
+
]);
|
|
100
|
+
});
|
|
101
|
+
|
|
102
|
+
it("walks THROUGH a blank-node record", async () => {
|
|
103
|
+
// The traversal a per-layer SPARQL query cannot do: a blank node's label is
|
|
104
|
+
// not a legal IRI, so a frontier query either fails or drops it silently.
|
|
105
|
+
const result = await ask("ds:bridgeStart", "ds:bridgeEnd");
|
|
106
|
+
expect(result.connected).toBe(true);
|
|
107
|
+
expect(result.hops).toBe(2);
|
|
108
|
+
const middle = result.paths[0]?.steps[0]?.to;
|
|
109
|
+
expect(middle?.addressable).toBe(false);
|
|
110
|
+
expect(result.paths[0]?.steps[1]?.from.value).toBe(middle?.value);
|
|
111
|
+
});
|
|
112
|
+
|
|
113
|
+
it("returns ONLY shortest paths, all of one length, ranked and distinct", async () => {
|
|
114
|
+
const result = await ask("ds:m1", "ds:m2");
|
|
115
|
+
expect(result.hops).toBe(2);
|
|
116
|
+
expect(result.pathCount).toBe(3);
|
|
117
|
+
for (const path of result.paths) expect(path.steps).toHaveLength(2);
|
|
118
|
+
// Ranked by fewest distinct predicates, then lexicographically: the two
|
|
119
|
+
// single-family routes come first, alpha before gamma, and the route that
|
|
120
|
+
// mixes two predicates comes last.
|
|
121
|
+
expect(result.paths.map((path) => path.steps[0]?.to.value)).toEqual([
|
|
122
|
+
`${DS}via1`,
|
|
123
|
+
`${DS}via3`,
|
|
124
|
+
`${DS}via2`,
|
|
125
|
+
]);
|
|
126
|
+
expect(result.truncated).toBeUndefined();
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
it("caps the paths shown but never the count, and says it truncated", async () => {
|
|
130
|
+
const capped = await ask("ds:m1", "ds:m2", { paths: 1 });
|
|
131
|
+
expect(capped.paths).toHaveLength(1);
|
|
132
|
+
expect(capped.pathCount).toBe(3);
|
|
133
|
+
expect(capped.truncated).toBe(true);
|
|
134
|
+
// A cap above the true count is not a truncation.
|
|
135
|
+
const generous = await ask("ds:m1", "ds:m2", { paths: 99 });
|
|
136
|
+
expect(generous.paths).toHaveLength(3);
|
|
137
|
+
expect(generous.truncated).toBeUndefined();
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
it("honours --hops, up to its ceiling, and refuses above it", async () => {
|
|
141
|
+
// The chain is longer than the ceiling, so it can be walked to the ceiling
|
|
142
|
+
// and still have a pair the ceiling cannot reach.
|
|
143
|
+
expect((await ask("ds:a1", "ds:a6")).connected).toBe(false);
|
|
144
|
+
const raised = await ask("ds:a1", "ds:a6", { hops: 5 });
|
|
145
|
+
expect(raised.connected).toBe(true);
|
|
146
|
+
expect(raised.hops).toBe(5);
|
|
147
|
+
expect(raised.hopLimit).toBe(5);
|
|
148
|
+
const ceiling = await ask("ds:a1", "ds:a7", { hops: 6 });
|
|
149
|
+
expect(ceiling.connected).toBe(true);
|
|
150
|
+
expect(ceiling.hops).toBe(6);
|
|
151
|
+
|
|
152
|
+
// Above the ceiling it REFUSES rather than clamping: a clamped limit is a
|
|
153
|
+
// limit the answer would report without the caller having set it.
|
|
154
|
+
const tooFar = await refusal("ds:a1", "ds:a7", { hops: 7 });
|
|
155
|
+
expect(tooFar.code).toBe("INVALID_INPUT");
|
|
156
|
+
expect(tooFar.message).toContain("7");
|
|
157
|
+
const notWhole = await refusal("ds:a1", "ds:a7", { hops: 2.5 });
|
|
158
|
+
expect(notWhole.code).toBe("INVALID_INPUT");
|
|
159
|
+
});
|
|
160
|
+
|
|
161
|
+
it("classifies each edge, not each predicate name", async () => {
|
|
162
|
+
// ONE predicate, two group sizes. `ds:shares` fans in past the threshold on
|
|
163
|
+
// ds:hub and stays under it on ds:small, so the same predicate is a roster
|
|
164
|
+
// at one node and a real relation at another. No rule keyed on a predicate
|
|
165
|
+
// NAME can produce both of these answers.
|
|
166
|
+
const relation = await ask("ds:relA", "ds:relB");
|
|
167
|
+
expect(relation.connected).toBe(true);
|
|
168
|
+
expect(relation.hops).toBe(2);
|
|
169
|
+
expect(relation.paths[0]?.steps[0]?.predicate.value).toBe(`${DS}shares`);
|
|
170
|
+
|
|
171
|
+
const roster = await ask("ds:member0", "ds:member1");
|
|
172
|
+
expect(roster.connected).toBe(false);
|
|
173
|
+
expect(
|
|
174
|
+
roster.sharedRosters.map((group) => group.predicate.value),
|
|
175
|
+
).toContain(`${DS}shares`);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("applies the rule in BOTH directions of a group", async () => {
|
|
179
|
+
// ds:fanHub fans OUT past the threshold, so its edges are refused for the
|
|
180
|
+
// (subject, predicate) half of the rule — the half a rule that only counted
|
|
181
|
+
// inbound fan-in would miss entirely.
|
|
182
|
+
const result = await ask("ds:leaf0", "ds:leaf1");
|
|
183
|
+
expect(result.connected).toBe(false);
|
|
184
|
+
expect(result.sharedRosters).toEqual([
|
|
185
|
+
{
|
|
186
|
+
side: "objects",
|
|
187
|
+
predicate: expect.objectContaining({ value: `${DS}fansTo` }),
|
|
188
|
+
node: expect.objectContaining({ value: `${DS}fanHub` }),
|
|
189
|
+
members: ROSTER_SIZE,
|
|
190
|
+
},
|
|
191
|
+
]);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("answers the same path set whichever way round it is asked", async () => {
|
|
195
|
+
const forwards = await ask("ds:m1", "ds:m2");
|
|
196
|
+
const backwards = await ask("ds:m2", "ds:m1");
|
|
197
|
+
expect(backwards.hops).toBe(forwards.hops);
|
|
198
|
+
expect(backwards.pathCount).toBe(forwards.pathCount);
|
|
199
|
+
// The same paths, walked the other way: reverse the step order, swap each
|
|
200
|
+
// step's ends, and flip its direction.
|
|
201
|
+
const mirrored = backwards.paths.map((path) =>
|
|
202
|
+
[...path.steps].reverse().map((step) => ({
|
|
203
|
+
from: step.to.value,
|
|
204
|
+
to: step.from.value,
|
|
205
|
+
predicate: step.predicate.value,
|
|
206
|
+
direction: step.direction === "forward" ? "reverse" : "forward",
|
|
207
|
+
})),
|
|
208
|
+
);
|
|
209
|
+
const original = forwards.paths.map((path) =>
|
|
210
|
+
path.steps.map((step) => ({
|
|
211
|
+
from: step.from.value,
|
|
212
|
+
to: step.to.value,
|
|
213
|
+
predicate: step.predicate.value,
|
|
214
|
+
direction: step.direction,
|
|
215
|
+
})),
|
|
216
|
+
);
|
|
217
|
+
expect(mirrored.map((p) => JSON.stringify(p)).sort()).toEqual(
|
|
218
|
+
original.map((p) => JSON.stringify(p)).sort(),
|
|
219
|
+
);
|
|
220
|
+
});
|
|
221
|
+
});
|
|
222
|
+
|
|
223
|
+
describe("graph connect — the five negative answers, each distinguishable", () => {
|
|
224
|
+
it("(a) refuses an absent name, naming WHICH argument, with suggestions", async () => {
|
|
225
|
+
const first = await refusal("ds:membre0", "ds:a1");
|
|
226
|
+
expect(first.code).toBe("ENTITY_NOT_FOUND");
|
|
227
|
+
expect(first.message).toContain("<a>");
|
|
228
|
+
expect(first.message).toContain("ds:membre0");
|
|
229
|
+
// A name-similarity suggestion, from the same index the argument's own
|
|
230
|
+
// shell completion draws on.
|
|
231
|
+
expect(first.suggestions).toContain("ds:member0");
|
|
232
|
+
// And never a connectivity VERDICT: a typo must not be reported as a fact
|
|
233
|
+
// about how two things relate. (Saying that nothing can be said is the
|
|
234
|
+
// opposite of a claim, so the check is for the verdicts themselves.)
|
|
235
|
+
expect(first.message).not.toMatch(
|
|
236
|
+
/not connected|unrelated|no path|no relation/i,
|
|
237
|
+
);
|
|
238
|
+
|
|
239
|
+
// The SECOND argument is named as the second argument.
|
|
240
|
+
const second = await refusal("ds:a1", "ds:nosuchthing");
|
|
241
|
+
expect(second.code).toBe("ENTITY_NOT_FOUND");
|
|
242
|
+
expect(second.message).toContain("<b>");
|
|
243
|
+
});
|
|
244
|
+
|
|
245
|
+
it("(b) answers that an endpoint carries no relation, and says what it is", async () => {
|
|
246
|
+
const result = await ask("ds:isolated", "ds:a1");
|
|
247
|
+
expect(result.connected).toBe(false);
|
|
248
|
+
expect(result.unconnectedBecause).toBe("endpoint-carries-no-relation");
|
|
249
|
+
expect(result.a.relatable).toBe(false);
|
|
250
|
+
expect(result.b.relatable).toBe(true);
|
|
251
|
+
// "Then what IS it?" — the groups that classify it, which is all there is
|
|
252
|
+
// to say about a node with no relations.
|
|
253
|
+
const classifiers = (result.a.classifiedBy ?? []).map(
|
|
254
|
+
(group) => group.predicate.value,
|
|
255
|
+
);
|
|
256
|
+
expect(classifiers).toContain(RDF_TYPE);
|
|
257
|
+
expect(classifiers).toContain(`${DS}shares`);
|
|
258
|
+
});
|
|
259
|
+
|
|
260
|
+
it("(c) answers that there is no path within the limit", async () => {
|
|
261
|
+
const result = await ask("ds:a1", `ds:a${CHAIN_LENGTH}`);
|
|
262
|
+
expect(result.connected).toBe(false);
|
|
263
|
+
expect(result.unconnectedBecause).toBe("no-path-within-hop-limit");
|
|
264
|
+
expect(result.hops).toBeNull();
|
|
265
|
+
expect(result.paths).toEqual([]);
|
|
266
|
+
// Nothing in common at all — which is what separates this from (d).
|
|
267
|
+
expect(result.sharedRosters).toEqual([]);
|
|
268
|
+
// Both endpoints ARE relatable, which is what separates it from (b).
|
|
269
|
+
expect(result.a.relatable).toBe(true);
|
|
270
|
+
expect(result.b.relatable).toBe(true);
|
|
271
|
+
});
|
|
272
|
+
|
|
273
|
+
it("(d) answers that the two share only membership, and names it", async () => {
|
|
274
|
+
const result = await ask("ds:member0", "ds:member1");
|
|
275
|
+
expect(result.connected).toBe(false);
|
|
276
|
+
expect(result.unconnectedBecause).toBe("shares-only-rosters");
|
|
277
|
+
expect(result.a.relatable).toBe(true);
|
|
278
|
+
expect(result.b.relatable).toBe(true);
|
|
279
|
+
const hub = result.sharedRosters.find(
|
|
280
|
+
(group) => group.node.value === `${DS}hub`,
|
|
281
|
+
);
|
|
282
|
+
expect(hub).toMatchObject({
|
|
283
|
+
side: "subjects",
|
|
284
|
+
members: HUB_ROSTER_SIZE,
|
|
285
|
+
});
|
|
286
|
+
});
|
|
287
|
+
|
|
288
|
+
it("(e) REFUSES when no loaded pack binds an endpoint's namespace", async () => {
|
|
289
|
+
// Staleness, not a fact about the model — and the dangerous one, because a
|
|
290
|
+
// missing pack makes the honest "no path" and the dishonest one identical.
|
|
291
|
+
// So it refuses with the store's own recovery instead of answering.
|
|
292
|
+
const result = await refusal("https://unloaded.example/thing", "ds:a1");
|
|
293
|
+
expect(result.code).toBe("STORE_UNAVAILABLE");
|
|
294
|
+
expect(result.message).toContain("https://unloaded.example/thing");
|
|
295
|
+
expect(result.message).not.toMatch(
|
|
296
|
+
/not connected|unrelated|no path|no relation/i,
|
|
297
|
+
);
|
|
298
|
+
// The recovery the kernel already mints for a store that cannot answer.
|
|
299
|
+
expect(
|
|
300
|
+
(result as { recovery?: { cli?: string; mcp?: { tool: string } } })
|
|
301
|
+
.recovery?.cli,
|
|
302
|
+
).toContain("sources update");
|
|
303
|
+
expect(
|
|
304
|
+
(result as { recovery?: { mcp?: { tool: string } } }).recovery?.mcp?.tool,
|
|
305
|
+
).toBe("sources_update");
|
|
306
|
+
});
|
|
307
|
+
|
|
308
|
+
it("keeps all five apart — five causes, five distinct discriminators", async () => {
|
|
309
|
+
const absent = await refusal("ds:nosuchthing", "ds:a1");
|
|
310
|
+
const unbound = await refusal("https://unloaded.example/thing", "ds:a1");
|
|
311
|
+
const answers = await Promise.all([
|
|
312
|
+
ask("ds:isolated", "ds:a1"),
|
|
313
|
+
ask("ds:a1", `ds:a${CHAIN_LENGTH}`),
|
|
314
|
+
ask("ds:member0", "ds:member1"),
|
|
315
|
+
]);
|
|
316
|
+
// The two refusals carry different codes...
|
|
317
|
+
expect(new Set([absent.code, unbound.code]).size).toBe(2);
|
|
318
|
+
// ...and the three answers carry different reasons, none of them empty.
|
|
319
|
+
const reasons = answers.map((answer) => answer.unconnectedBecause);
|
|
320
|
+
expect(new Set(reasons).size).toBe(3);
|
|
321
|
+
for (const answer of answers) {
|
|
322
|
+
expect(answer.connected).toBe(false);
|
|
323
|
+
expect(answer.unconnectedBecause).toBeDefined();
|
|
324
|
+
}
|
|
325
|
+
});
|
|
326
|
+
});
|
|
327
|
+
|
|
328
|
+
describe("graph connect — what every answer states about itself", () => {
|
|
329
|
+
it("reports the packs the answering store was actually built from", async () => {
|
|
330
|
+
const session = await rt.store.get();
|
|
331
|
+
const result = await ask("ds:a1", "ds:a2");
|
|
332
|
+
expect(result.packs).toEqual(
|
|
333
|
+
session.manifest.sourceRef
|
|
334
|
+
.split(",")
|
|
335
|
+
.map((ref) => ref.trim())
|
|
336
|
+
.filter((ref) => ref.length > 0),
|
|
337
|
+
);
|
|
338
|
+
});
|
|
339
|
+
|
|
340
|
+
it("states its policy on a NEGATIVE answer too", async () => {
|
|
341
|
+
// The cheapest answer is the one most in need of context: without the
|
|
342
|
+
// limit, the threshold and the packs, "not connected" is unfalsifiable.
|
|
343
|
+
const result = await ask("ds:a1", `ds:a${CHAIN_LENGTH}`, { hops: 3 });
|
|
344
|
+
expect(result.hopLimit).toBe(3);
|
|
345
|
+
expect(result.rosterThreshold).toBe(ROSTER_THRESHOLD);
|
|
346
|
+
expect(result.packs.length).toBeGreaterThan(0);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("refuses two spellings of the same entity rather than answering 0 hops", async () => {
|
|
350
|
+
const result = await refusal("ds:a1", `${DS}a1`);
|
|
351
|
+
expect(result.code).toBe("INVALID_INPUT");
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
it("refuses an unknown prefix as invalid input, naming the loaded ones", async () => {
|
|
355
|
+
const result = await refusal("nope:a1", "ds:a1");
|
|
356
|
+
expect(result.code).toBe("INVALID_INPUT");
|
|
357
|
+
expect((result as { validOptions?: string[] }).validOptions).toContain(
|
|
358
|
+
"ds",
|
|
359
|
+
);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
@@ -0,0 +1,136 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The shape of a `graph connect` answer — the ONE contract its run body writes
|
|
3
|
+
* and its three formatters read.
|
|
4
|
+
*
|
|
5
|
+
* Type-only, and deliberately its own module: the formatters hang off the verb
|
|
6
|
+
* spec, so they are on the static import graph the lazy-dispatch probe walks
|
|
7
|
+
* (`capabilities/lazy.test.ts`), while the walk itself must stay behind a
|
|
8
|
+
* dynamic import. A shared type module lets both sides name the same answer
|
|
9
|
+
* without dragging the traversal onto the storeless fast path.
|
|
10
|
+
*
|
|
11
|
+
* Every answer carries the POLICY it was produced under — the hop limit, the
|
|
12
|
+
* roster threshold, and the packs searched. That is Constitution VI applied to
|
|
13
|
+
* the answer rather than only to the source: the commonest answer this verb
|
|
14
|
+
* gives is "these two are not connected", and that sentence means nothing
|
|
15
|
+
* without knowing how far it looked, what it was willing to walk, and what it
|
|
16
|
+
* was looking in. A missing pack is what makes an honest "no path" and a
|
|
17
|
+
* dishonest one byte-identical, so `packs` is present whether the answer is
|
|
18
|
+
* positive or negative.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
/** A node or predicate as the answer names it: full IRI, short form, human name. */
|
|
22
|
+
export interface ConnectTerm {
|
|
23
|
+
/** The full IRI, or a blank node's session-local label. */
|
|
24
|
+
readonly value: string;
|
|
25
|
+
/** Prefixed form, when a loaded namespace matches. */
|
|
26
|
+
readonly prefixed?: string;
|
|
27
|
+
/** Human name from the pack index, when it says more than the local name. */
|
|
28
|
+
readonly title?: string;
|
|
29
|
+
/**
|
|
30
|
+
* Marks a term no `pragma:{+uri}` read can resolve. Only blank nodes carry
|
|
31
|
+
* it: their labels re-mint on every load, so the record is real but its
|
|
32
|
+
* handle is not addressable. Blank nodes ARE walked — 6,942 of them carry the
|
|
33
|
+
* token graph's declarations and resolved values — so a path may name one.
|
|
34
|
+
*/
|
|
35
|
+
readonly addressable?: false;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* One step of a path: the triple that was traversed, and which way.
|
|
40
|
+
*
|
|
41
|
+
* `direction` is not decoration. `forward` says the triple `(from, predicate,
|
|
42
|
+
* to)` is asserted; `reverse` says `(to, predicate, from)` is. Concatenating a
|
|
43
|
+
* path's steps walks from one endpoint to the other only if each step's
|
|
44
|
+
* direction is respected, and an answer that dropped it would claim edges the
|
|
45
|
+
* graph does not assert.
|
|
46
|
+
*/
|
|
47
|
+
export interface ConnectStep {
|
|
48
|
+
readonly from: ConnectTerm;
|
|
49
|
+
readonly predicate: ConnectTerm;
|
|
50
|
+
readonly direction: "forward" | "reverse";
|
|
51
|
+
readonly to: ConnectTerm;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** One path: an ordered walk of steps from `a` to `b`. */
|
|
55
|
+
export interface ConnectPath {
|
|
56
|
+
readonly steps: readonly ConnectStep[];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* A group both endpoints belong to that is a ROSTER rather than a relation —
|
|
61
|
+
* membership, which is what the noun's own list verb answers properly.
|
|
62
|
+
*
|
|
63
|
+
* Two shapes, because a group has two directions: `subjects` means both
|
|
64
|
+
* endpoints are subjects of `predicate` pointing at `node` (both are
|
|
65
|
+
* `ds:Component`s, both in the `ds:global` tier); `objects` means both are
|
|
66
|
+
* objects of `node` asserting `predicate` (both covered by one modifier
|
|
67
|
+
* family's 359 `dt:covers` edges). Reporting these is what separates "unrelated
|
|
68
|
+
* in the model, though they do share membership" from "unrelated, full stop".
|
|
69
|
+
*/
|
|
70
|
+
export interface SharedRoster {
|
|
71
|
+
/** Which side of the group the two endpoints sit on. */
|
|
72
|
+
readonly side: "subjects" | "objects";
|
|
73
|
+
readonly predicate: ConnectTerm;
|
|
74
|
+
/** The group's other end: the shared object, or the shared subject. */
|
|
75
|
+
readonly node: ConnectTerm;
|
|
76
|
+
/** The TRUE size of the group — why it is a roster and not a relation. */
|
|
77
|
+
readonly members: number;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One endpoint of the question, as the store resolved it. */
|
|
81
|
+
export interface ConnectEndpoint {
|
|
82
|
+
/** The value the caller supplied, verbatim. */
|
|
83
|
+
readonly input: string;
|
|
84
|
+
readonly term: ConnectTerm;
|
|
85
|
+
/**
|
|
86
|
+
* False when this endpoint carries no walkable relation edge in either
|
|
87
|
+
* direction — it is in the graph, but only classified. Nothing can be
|
|
88
|
+
* connected to it, which is a different answer from "nothing is".
|
|
89
|
+
*/
|
|
90
|
+
readonly relatable: boolean;
|
|
91
|
+
/**
|
|
92
|
+
* The roster groups that classify an endpoint, present only when
|
|
93
|
+
* {@link relatable} is false — the answer to "then what IS it?", which is
|
|
94
|
+
* the only useful thing to say about a node with no relations.
|
|
95
|
+
*/
|
|
96
|
+
readonly classifiedBy?: readonly SharedRoster[];
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Why an unconnected pair is unconnected. Three of `CN.07`'s five cases; the
|
|
101
|
+
* other two are refusals rather than answers (an absent name throws
|
|
102
|
+
* `ENTITY_NOT_FOUND` naming which argument, an unbound namespace throws
|
|
103
|
+
* `STORE_UNAVAILABLE`), because neither may make a connectivity claim at all.
|
|
104
|
+
*/
|
|
105
|
+
export type ConnectRefusal =
|
|
106
|
+
/** An endpoint carries no relation edge — see {@link ConnectEndpoint.relatable}. */
|
|
107
|
+
| "endpoint-carries-no-relation"
|
|
108
|
+
/** The walk exhausted, but the two share membership — {@link ConnectResult.sharedRosters}. */
|
|
109
|
+
| "shares-only-rosters"
|
|
110
|
+
/** The walk exhausted with nothing in common at all. */
|
|
111
|
+
| "no-path-within-hop-limit";
|
|
112
|
+
|
|
113
|
+
/** The answer to one `graph connect` question. */
|
|
114
|
+
export interface ConnectResult {
|
|
115
|
+
readonly a: ConnectEndpoint;
|
|
116
|
+
readonly b: ConnectEndpoint;
|
|
117
|
+
readonly connected: boolean;
|
|
118
|
+
/** Which negative answer this is; absent when {@link connected}. */
|
|
119
|
+
readonly unconnectedBecause?: ConnectRefusal;
|
|
120
|
+
/** The shortest-path length, or null when there is no path. */
|
|
121
|
+
readonly hops: number | null;
|
|
122
|
+
/** How many distinct shortest paths there are — the truth behind `paths`. */
|
|
123
|
+
readonly pathCount: number;
|
|
124
|
+
/** The shortest paths, ranked and capped by `--paths`. */
|
|
125
|
+
readonly paths: readonly ConnectPath[];
|
|
126
|
+
/** Set when `paths` is shorter than {@link pathCount}. */
|
|
127
|
+
readonly truncated?: true;
|
|
128
|
+
/** Roster groups both endpoints belong to (see {@link SharedRoster}). */
|
|
129
|
+
readonly sharedRosters: readonly SharedRoster[];
|
|
130
|
+
/** The hop limit actually applied to this walk. */
|
|
131
|
+
readonly hopLimit: number;
|
|
132
|
+
/** The fan-in above which an edge was refused as a roster. */
|
|
133
|
+
readonly rosterThreshold: number;
|
|
134
|
+
/** The packs the answering store was built from, as it records them. */
|
|
135
|
+
readonly packs: readonly string[];
|
|
136
|
+
}
|
|
@@ -0,0 +1,165 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graph connect <a> <b>` — what connects two entities, and what "unlinked"
|
|
3
|
+
* means when nothing does.
|
|
4
|
+
*
|
|
5
|
+
* The third verb on the `graph` noun, beside `inspect` (one entity's
|
|
6
|
+
* neighbourhood) and `query` (the raw SPARQL escape hatch). It belongs here for
|
|
7
|
+
* the same reason those two do: it reads ACROSS the whole store rather than
|
|
8
|
+
* within one population, so it hangs off the noun for a store-wide read and
|
|
9
|
+
* adds no noun of its own.
|
|
10
|
+
*
|
|
11
|
+
* It is hand-written rather than a declared pack story because the pack grammar
|
|
12
|
+
* cannot express it. A list story's parameters are exactly its declared filters
|
|
13
|
+
* plus its declared search, every filter is projected as an OPTIONAL NAMED
|
|
14
|
+
* flag, and the only positional-and-required parameter the compiler ever mints
|
|
15
|
+
* is a lookup's single name — so there is nowhere in the grammar to put two
|
|
16
|
+
* required entity arguments. And a filter is a row predicate over an author's
|
|
17
|
+
* query, so a declared form would have to enumerate the walk space and let a
|
|
18
|
+
* filter pick one pair out of it: at four hops that space is 71 million walks,
|
|
19
|
+
* against the largest declared story's 1,698 rows.
|
|
20
|
+
*
|
|
21
|
+
* Both arguments accept exactly what `graph inspect` accepts — a prefixed name
|
|
22
|
+
* or an absolute IRI — and complete from the pack index through the same
|
|
23
|
+
* `names`/`index` source a compiled lookup's `<name>` uses, so the two
|
|
24
|
+
* positionals behave like every other entity argument in the CLI.
|
|
25
|
+
*
|
|
26
|
+
* Store-backed and read-only, like both its siblings. The traversal lives in a
|
|
27
|
+
* dynamically-imported run body: the relation index it needs is built by one
|
|
28
|
+
* scan of the store, and a process that never asks a connect question must
|
|
29
|
+
* never pay for it — which is the same guarantee `needsStore` and the lazy
|
|
30
|
+
* store already give every other read.
|
|
31
|
+
*/
|
|
32
|
+
|
|
33
|
+
import { BIN_NAME } from "../../constants.js";
|
|
34
|
+
import { PragmaError } from "../../kernel/error/index.js";
|
|
35
|
+
import { asVerb } from "../../kernel/spec/asVerb.js";
|
|
36
|
+
import type { VerbSpec } from "../../kernel/spec/index.js";
|
|
37
|
+
import { connectFormatters } from "./connect.render.js";
|
|
38
|
+
import type { ConnectResult } from "./connect.types.js";
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The hop limit in force unless `--hops` says otherwise.
|
|
42
|
+
*
|
|
43
|
+
* Four, from the measured connectivity ladder on the shipped pack: it is the
|
|
44
|
+
* smallest limit that spans the longest genuine bridge in this store — the walk
|
|
45
|
+
* from a platform variable through its declaration and its references to the
|
|
46
|
+
* symbol behind it — and the largest that keeps the answer set honest. At three
|
|
47
|
+
* that walk is refused; at six the connected fraction jumps and the paths stop
|
|
48
|
+
* being something a person can hold in mind.
|
|
49
|
+
*/
|
|
50
|
+
export const DEFAULT_HOPS = 4;
|
|
51
|
+
|
|
52
|
+
/** The ceiling `--hops` may be raised to; above it the verb refuses. */
|
|
53
|
+
export const MAX_HOPS = 6;
|
|
54
|
+
|
|
55
|
+
/** How many shortest paths are shown unless `--paths` says otherwise. */
|
|
56
|
+
export const DEFAULT_PATHS = 10;
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Hold a numeric param to a whole number within its stated range.
|
|
60
|
+
*
|
|
61
|
+
* REFUSES rather than clamps. A clamped `--hops 40` would answer a question
|
|
62
|
+
* nobody asked and report a limit the caller did not set, which is precisely
|
|
63
|
+
* the hidden policy every answer's policy block exists to rule out.
|
|
64
|
+
*
|
|
65
|
+
* The kernel already coerces a `number` param and rejects what is not a number
|
|
66
|
+
* at all (`project/cli/dispatch.ts#coerceParam`, and `z.number()` over MCP), so
|
|
67
|
+
* what is left to check is RANGE and wholeness — `--hops 2.5` is a number, and
|
|
68
|
+
* it is not a hop count.
|
|
69
|
+
*
|
|
70
|
+
* @param value - The coerced param value.
|
|
71
|
+
* @param name - The flag's name, for the diagnostic.
|
|
72
|
+
* @param fallback - The value when the flag was not given.
|
|
73
|
+
* @param min - The smallest accepted value.
|
|
74
|
+
* @param max - The largest accepted value, if there is one.
|
|
75
|
+
* @returns The validated integer.
|
|
76
|
+
* @throws PragmaError INVALID_INPUT when the value is not an integer in range.
|
|
77
|
+
*/
|
|
78
|
+
function bounded(
|
|
79
|
+
value: unknown,
|
|
80
|
+
name: string,
|
|
81
|
+
fallback: number,
|
|
82
|
+
min: number,
|
|
83
|
+
max?: number,
|
|
84
|
+
): number {
|
|
85
|
+
if (value === undefined || value === null) return fallback;
|
|
86
|
+
const parsed = typeof value === "number" ? value : Number(value);
|
|
87
|
+
const ceiling = max === undefined ? "" : ` and at most ${max}`;
|
|
88
|
+
if (
|
|
89
|
+
!Number.isInteger(parsed) ||
|
|
90
|
+
parsed < min ||
|
|
91
|
+
(max !== undefined && parsed > max)
|
|
92
|
+
) {
|
|
93
|
+
throw PragmaError.invalidInput(name, String(value), {
|
|
94
|
+
recovery: {
|
|
95
|
+
message: `--${name} must be a whole number of at least ${min}${ceiling}.`,
|
|
96
|
+
},
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
return parsed;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const connectVerb: VerbSpec<Record<string, unknown>, ConnectResult> = {
|
|
103
|
+
path: ["graph", "connect"],
|
|
104
|
+
summary: "Show the shortest relation paths between two entities.",
|
|
105
|
+
doc: "Finds every shortest path of RELATION edges between two entities, and says which kind of nothing it found when there is no path. An edge counts as a relation only where it does not fan out into a roster — membership of a class, a tier or a family is answered by that noun's list verb, not by a path — so most pairs are correctly reported as unconnected. Address each endpoint by prefixed name (ds:global.component.button) or absolute IRI. Every answer states the hop limit, the fan-in threshold, and the packs searched.",
|
|
106
|
+
params: [
|
|
107
|
+
{
|
|
108
|
+
kind: "string",
|
|
109
|
+
name: "a",
|
|
110
|
+
doc: "The first endpoint — a prefixed name or absolute IRI.",
|
|
111
|
+
positional: true,
|
|
112
|
+
required: true,
|
|
113
|
+
complete: { kind: "names", source: { from: "index", type: "" } },
|
|
114
|
+
},
|
|
115
|
+
{
|
|
116
|
+
kind: "string",
|
|
117
|
+
name: "b",
|
|
118
|
+
doc: "The second endpoint — a prefixed name or absolute IRI.",
|
|
119
|
+
positional: true,
|
|
120
|
+
required: true,
|
|
121
|
+
complete: { kind: "names", source: { from: "index", type: "" } },
|
|
122
|
+
},
|
|
123
|
+
{
|
|
124
|
+
kind: "number",
|
|
125
|
+
name: "hops",
|
|
126
|
+
doc: `Largest path length to return (default ${DEFAULT_HOPS}, ceiling ${MAX_HOPS}).`,
|
|
127
|
+
default: DEFAULT_HOPS,
|
|
128
|
+
},
|
|
129
|
+
{
|
|
130
|
+
kind: "number",
|
|
131
|
+
name: "paths",
|
|
132
|
+
doc: `Most shortest paths to show (default ${DEFAULT_PATHS}); the answer always reports the true total.`,
|
|
133
|
+
default: DEFAULT_PATHS,
|
|
134
|
+
},
|
|
135
|
+
],
|
|
136
|
+
output: { formatters: connectFormatters },
|
|
137
|
+
examples: [
|
|
138
|
+
{
|
|
139
|
+
cmd: `${BIN_NAME} graph connect ds:global.component.button dt:color.text`,
|
|
140
|
+
note: "a block and a token symbol",
|
|
141
|
+
},
|
|
142
|
+
{
|
|
143
|
+
cmd: `${BIN_NAME} graph connect ds:global.component.button ds:apps.pattern.datatable --format json`,
|
|
144
|
+
note: "an honest unlinked answer, with the policy it was found under",
|
|
145
|
+
},
|
|
146
|
+
],
|
|
147
|
+
capability: {
|
|
148
|
+
needsStore: true,
|
|
149
|
+
mutates: false,
|
|
150
|
+
mcp: {
|
|
151
|
+
expose: true,
|
|
152
|
+
annotations: { readOnlyHint: true, openWorldHint: false },
|
|
153
|
+
},
|
|
154
|
+
},
|
|
155
|
+
run: (params: Record<string, unknown>, rt) =>
|
|
156
|
+
import("./runConnect.js").then((m) =>
|
|
157
|
+
m.runConnect(rt, String(params.a), String(params.b), {
|
|
158
|
+
hops: bounded(params.hops, "hops", DEFAULT_HOPS, 1, MAX_HOPS),
|
|
159
|
+
paths: bounded(params.paths, "paths", DEFAULT_PATHS, 1),
|
|
160
|
+
}),
|
|
161
|
+
),
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
/** The `graph connect` verb. */
|
|
165
|
+
export const graphConnectVerb = asVerb(connectVerb);
|
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* The `graph` capability module — `inspect` (single-entity read)
|
|
3
|
-
*
|
|
2
|
+
* The `graph` capability module — `inspect` (single-entity read), `query` (the
|
|
3
|
+
* raw SPARQL escape hatch, PR6) and `connect` (relation paths between two
|
|
4
|
+
* entities). The covenant blesses all three.
|
|
4
5
|
*/
|
|
5
6
|
|
|
6
7
|
import type { CapabilityModule } from "../../kernel/spec/index.js";
|
|
7
8
|
import { resourceProvider } from "../resources/index.js";
|
|
9
|
+
import { graphConnectVerb } from "./connect.verb.js";
|
|
8
10
|
import { graphInspectVerb } from "./inspect.verb.js";
|
|
9
11
|
import { graphQueryVerb } from "./query.verb.js";
|
|
10
12
|
|
|
@@ -16,6 +18,6 @@ import { graphQueryVerb } from "./query.verb.js";
|
|
|
16
18
|
*/
|
|
17
19
|
export const graphModule: CapabilityModule = {
|
|
18
20
|
name: "graph",
|
|
19
|
-
verbs: [graphInspectVerb, graphQueryVerb],
|
|
21
|
+
verbs: [graphConnectVerb, graphInspectVerb, graphQueryVerb],
|
|
20
22
|
mcpResources: resourceProvider,
|
|
21
23
|
};
|