@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,577 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The `graph connect` run body (lazily imported, off the fast path).
|
|
3
|
+
*
|
|
4
|
+
* Answers one question — what connects these two entities — and, far more
|
|
5
|
+
* often, says honestly that nothing does. On the pack this CLI ships, 97% of
|
|
6
|
+
* arbitrary pairs are not connected by any relation within the hop limit, so
|
|
7
|
+
* the negative answer is the primary output rather than an error or an empty
|
|
8
|
+
* result. It has five causes and they must never be conflated:
|
|
9
|
+
*
|
|
10
|
+
* | Case | Detected by | Shape |
|
|
11
|
+
* |---|---|---|
|
|
12
|
+
* | the namespace is bound by no loaded pack | the resolved IRI against the session's prefix map | REFUSES — `STORE_UNAVAILABLE` |
|
|
13
|
+
* | the name is not in the store | one `ASK` per endpoint, both positions | REFUSES — `ENTITY_NOT_FOUND`, naming which argument |
|
|
14
|
+
* | it is there, but carries no relation | absent from the relation index | answer, `endpoint-carries-no-relation` |
|
|
15
|
+
* | a path exists only through rosters | the walk exhausts and the two share a roster group | answer, `shares-only-rosters` |
|
|
16
|
+
* | no path within the limit | the walk exhausts with nothing shared | answer, `no-path-within-hop-limit` |
|
|
17
|
+
*
|
|
18
|
+
* The first two REFUSE rather than answer, and that is the point of the
|
|
19
|
+
* ordering. Staleness is checked before existence because a pack that is not
|
|
20
|
+
* loaded makes an honest "no path" and a dishonest one byte-identical; and an
|
|
21
|
+
* absent name is checked before any traversal because "I have never heard of
|
|
22
|
+
* this" must never be dressed up as "these are unrelated". Neither refusal makes
|
|
23
|
+
* a connectivity claim of any kind.
|
|
24
|
+
*
|
|
25
|
+
* The walk itself is a BOUNDED BIDIRECTIONAL breadth-first search over the
|
|
26
|
+
* session's relation index, in code. A SPARQL property path was measured and
|
|
27
|
+
* rejected: with a wildcard predicate every reachability question answers true,
|
|
28
|
+
* and — fatally — a property path returns reachability, never the path, which
|
|
29
|
+
* is the whole answer. A per-layer frontier query cannot re-bind a blank node
|
|
30
|
+
* by name, so it silently drops the 6,942 blank nodes carrying the token
|
|
31
|
+
* graph's records from the frontier. This walk traverses them.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import type { QueryResult } from "@canonical/ke";
|
|
35
|
+
import { cliRecovery, PragmaError } from "../../kernel/error/index.js";
|
|
36
|
+
import { resolveUri } from "../../kernel/packs/iri.js";
|
|
37
|
+
import { suggestNames } from "../../kernel/project/cli/suggestNames.js";
|
|
38
|
+
import { ROSTER_THRESHOLD } from "../../kernel/runtime/readEntity.js";
|
|
39
|
+
import { storeUnavailable } from "../../kernel/runtime/storeReadiness.js";
|
|
40
|
+
import type {
|
|
41
|
+
PragmaRuntime,
|
|
42
|
+
StoreSession,
|
|
43
|
+
} from "../../kernel/runtime/types.js";
|
|
44
|
+
import type {
|
|
45
|
+
ConnectEndpoint,
|
|
46
|
+
ConnectPath,
|
|
47
|
+
ConnectResult,
|
|
48
|
+
ConnectStep,
|
|
49
|
+
} from "./connect.types.js";
|
|
50
|
+
import type { RelationEdge, RelationIndex } from "./relationIndex.js";
|
|
51
|
+
import {
|
|
52
|
+
relationIndex,
|
|
53
|
+
rosterMemberships,
|
|
54
|
+
sharedRosters,
|
|
55
|
+
} from "./relationIndex.js";
|
|
56
|
+
|
|
57
|
+
/**
|
|
58
|
+
* A safety ceiling on how many distinct shortest paths are ENUMERATED.
|
|
59
|
+
*
|
|
60
|
+
* Not a cap on the answer — `--paths` is that — but a bound on the cross
|
|
61
|
+
* product a pathological pair could produce. Stated rather than implicit, and
|
|
62
|
+
* far above what the graph produces: the measured maximum over a thousand
|
|
63
|
+
* random pairs is 18. When it bites, `pathCount` is that ceiling and
|
|
64
|
+
* `truncated` is set, so the answer never claims to have counted more than it
|
|
65
|
+
* looked at.
|
|
66
|
+
*/
|
|
67
|
+
const ENUMERATION_CEILING = 5000;
|
|
68
|
+
|
|
69
|
+
/** Field separator for composite keys — illegal in an IRI, so unambiguous. */
|
|
70
|
+
const SEP = " ";
|
|
71
|
+
|
|
72
|
+
/** How a node was reached: from a neighbour one layer nearer the source. */
|
|
73
|
+
interface Arrival {
|
|
74
|
+
readonly other: string;
|
|
75
|
+
readonly predicate: string;
|
|
76
|
+
/** True when the asserted triple is `(other, predicate, this node)`. */
|
|
77
|
+
readonly forward: boolean;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One side of the bidirectional search. */
|
|
81
|
+
interface Side {
|
|
82
|
+
readonly dist: Map<string, number>;
|
|
83
|
+
readonly arrivals: Map<string, Arrival[]>;
|
|
84
|
+
frontier: string[];
|
|
85
|
+
radius: number;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/** Options a caller may set on a connect question. */
|
|
89
|
+
export interface ConnectOptions {
|
|
90
|
+
readonly hops: number;
|
|
91
|
+
readonly paths: number;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Answer one connect question.
|
|
96
|
+
*
|
|
97
|
+
* @param rt - The per-invocation runtime (its store is booted by the dispatcher).
|
|
98
|
+
* @param a - The first endpoint, as a prefixed name or an absolute IRI.
|
|
99
|
+
* @param b - The second endpoint, in the same forms.
|
|
100
|
+
* @param options - The hop limit and path cap in force.
|
|
101
|
+
* @returns The answer: the shortest paths, or which kind of nothing this is.
|
|
102
|
+
* @throws PragmaError STORE_UNAVAILABLE when an endpoint's namespace is bound
|
|
103
|
+
* by no loaded pack; ENTITY_NOT_FOUND when an endpoint is not in the store;
|
|
104
|
+
* INVALID_INPUT when the two endpoints are the same entity.
|
|
105
|
+
* @note Impure — boots the store, and scans it once per session.
|
|
106
|
+
*/
|
|
107
|
+
export async function runConnect(
|
|
108
|
+
rt: PragmaRuntime,
|
|
109
|
+
a: string,
|
|
110
|
+
b: string,
|
|
111
|
+
options: ConnectOptions,
|
|
112
|
+
): Promise<ConnectResult> {
|
|
113
|
+
const session = await rt.store.get();
|
|
114
|
+
const resolvedA = resolveUri(a, session.prefixes);
|
|
115
|
+
const resolvedB = resolveUri(b, session.prefixes);
|
|
116
|
+
|
|
117
|
+
// (e) Staleness first. A namespace no loaded pack binds cannot be reasoned
|
|
118
|
+
// about at all, and the answer it would otherwise get — "not connected" — is
|
|
119
|
+
// the one wrong answer this verb must never give.
|
|
120
|
+
assertNamespaceLoaded(session, a, resolvedA);
|
|
121
|
+
assertNamespaceLoaded(session, b, resolvedB);
|
|
122
|
+
|
|
123
|
+
if (resolvedA === resolvedB) {
|
|
124
|
+
throw PragmaError.invalidInput("arguments", `${a} / ${b}`, {
|
|
125
|
+
recovery: {
|
|
126
|
+
message:
|
|
127
|
+
"Both arguments name the same entity. Give two different endpoints, or inspect the one.",
|
|
128
|
+
},
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// (a) Existence, before any traversal, in EITHER position — an entity that is
|
|
133
|
+
// only ever an object is still in the graph.
|
|
134
|
+
await assertPresent(rt, session, "<a>", a, resolvedA);
|
|
135
|
+
await assertPresent(rt, session, "<b>", b, resolvedB);
|
|
136
|
+
|
|
137
|
+
const index = await relationIndex({ session, sparql: rt.query.sparql });
|
|
138
|
+
const shared = sharedRosters(index, resolvedA, resolvedB);
|
|
139
|
+
const endpointA = endpoint(index, a, resolvedA);
|
|
140
|
+
const endpointB = endpoint(index, b, resolvedB);
|
|
141
|
+
|
|
142
|
+
const policy = {
|
|
143
|
+
sharedRosters: shared,
|
|
144
|
+
hopLimit: options.hops,
|
|
145
|
+
rosterThreshold: ROSTER_THRESHOLD,
|
|
146
|
+
packs: loadedPacks(session),
|
|
147
|
+
} as const;
|
|
148
|
+
|
|
149
|
+
// (b) An endpoint with no relation at all: nothing CAN be connected to it,
|
|
150
|
+
// which is a different statement from "nothing is".
|
|
151
|
+
if (!endpointA.relatable || !endpointB.relatable) {
|
|
152
|
+
return {
|
|
153
|
+
a: endpointA,
|
|
154
|
+
b: endpointB,
|
|
155
|
+
connected: false,
|
|
156
|
+
unconnectedBecause: "endpoint-carries-no-relation",
|
|
157
|
+
hops: null,
|
|
158
|
+
pathCount: 0,
|
|
159
|
+
paths: [],
|
|
160
|
+
...policy,
|
|
161
|
+
};
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
const found = walk(index, resolvedA, resolvedB, options.hops);
|
|
165
|
+
if (found === undefined) {
|
|
166
|
+
return {
|
|
167
|
+
a: endpointA,
|
|
168
|
+
b: endpointB,
|
|
169
|
+
connected: false,
|
|
170
|
+
// (d) versus (c): sharing membership is a real, if lesser, relationship,
|
|
171
|
+
// and an answer that could not tell the two apart would be one answer
|
|
172
|
+
// where the graph holds two.
|
|
173
|
+
unconnectedBecause:
|
|
174
|
+
shared.length > 0 ? "shares-only-rosters" : "no-path-within-hop-limit",
|
|
175
|
+
hops: null,
|
|
176
|
+
pathCount: 0,
|
|
177
|
+
paths: [],
|
|
178
|
+
...policy,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
const ranked = rank(found.paths.map((steps) => ({ steps })));
|
|
183
|
+
const shown = ranked.slice(0, options.paths);
|
|
184
|
+
return {
|
|
185
|
+
a: endpointA,
|
|
186
|
+
b: endpointB,
|
|
187
|
+
connected: true,
|
|
188
|
+
hops: found.hops,
|
|
189
|
+
pathCount: ranked.length,
|
|
190
|
+
paths: shown,
|
|
191
|
+
...(shown.length < ranked.length ? { truncated: true as const } : {}),
|
|
192
|
+
...policy,
|
|
193
|
+
};
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
/** The packs the answering store records itself as having been built from. */
|
|
197
|
+
function loadedPacks(session: StoreSession): string[] {
|
|
198
|
+
return session.manifest.sourceRef
|
|
199
|
+
.split(",")
|
|
200
|
+
.map((ref) => ref.trim())
|
|
201
|
+
.filter((ref) => ref.length > 0);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/**
|
|
205
|
+
* Refuse when an endpoint's namespace is bound by no loaded pack.
|
|
206
|
+
*
|
|
207
|
+
* This is the staleness case, and it is the dangerous one: on a store missing a
|
|
208
|
+
* pack, the honest "these are not connected" and the dishonest one are the same
|
|
209
|
+
* bytes. So it refuses with the store-unavailable guidance the kernel already
|
|
210
|
+
* mints, rather than answering.
|
|
211
|
+
*
|
|
212
|
+
* The check is COVERAGE by the loaded prefix map, never population of the
|
|
213
|
+
* namespace. A namespace a loaded pack declares but asserts nothing in
|
|
214
|
+
* (`dcterms:`, `dso:` on the shipped pack) is a genuine not-found, and refusing
|
|
215
|
+
* there would be a false alarm on a perfectly healthy store. A prefixed name
|
|
216
|
+
* whose PREFIX is unknown never reaches here — `resolveUri` rejects it as
|
|
217
|
+
* invalid input, naming every prefix that is loaded.
|
|
218
|
+
*/
|
|
219
|
+
function assertNamespaceLoaded(
|
|
220
|
+
session: StoreSession,
|
|
221
|
+
input: string,
|
|
222
|
+
resolved: string,
|
|
223
|
+
): void {
|
|
224
|
+
for (const namespace of Object.values(session.prefixes)) {
|
|
225
|
+
if (resolved.startsWith(namespace)) return;
|
|
226
|
+
}
|
|
227
|
+
throw storeUnavailable(
|
|
228
|
+
`No loaded pack binds the namespace of "${input}", so this store cannot say what it relates to`,
|
|
229
|
+
);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Refuse when an endpoint is not in the store, naming WHICH argument.
|
|
234
|
+
*
|
|
235
|
+
* Never a connectivity claim: a caller who mistyped a name must not be told
|
|
236
|
+
* that two things are unrelated, and the pair that is a not-found today
|
|
237
|
+
* (`dt:modifier.color.text`, one of the channel symbols still to be minted) is
|
|
238
|
+
* exactly the pair someone would ask about first.
|
|
239
|
+
*
|
|
240
|
+
* The suggestion comes from the pack index — the same source the argument's own
|
|
241
|
+
* shell completion draws on, so a name the tab key would have offered is a name
|
|
242
|
+
* the error offers too.
|
|
243
|
+
*/
|
|
244
|
+
async function assertPresent(
|
|
245
|
+
rt: Pick<PragmaRuntime, "query">,
|
|
246
|
+
session: StoreSession,
|
|
247
|
+
label: string,
|
|
248
|
+
input: string,
|
|
249
|
+
resolved: string,
|
|
250
|
+
): Promise<void> {
|
|
251
|
+
const present = await rt.query.sparql(
|
|
252
|
+
`ASK { { <${resolved}> ?p ?o } UNION { ?s ?q <${resolved}> } UNION { ?s2 <${resolved}> ?o2 } }`,
|
|
253
|
+
);
|
|
254
|
+
if (asked(present)) return;
|
|
255
|
+
const candidates = session.index.entities.map((entity) => entity.name);
|
|
256
|
+
throw new PragmaError({
|
|
257
|
+
code: "ENTITY_NOT_FOUND",
|
|
258
|
+
message: `Argument ${label} "${input}" is not in the loaded graph, so nothing can be said about what it connects to.`,
|
|
259
|
+
entity: { type: "entity", name: input },
|
|
260
|
+
suggestions: suggestNames(input, candidates),
|
|
261
|
+
// `cliRecovery` prepends the distribution's own binary name, so the
|
|
262
|
+
// command passed here is the SUFFIX only.
|
|
263
|
+
recovery: cliRecovery(
|
|
264
|
+
`graph inspect ${input}`,
|
|
265
|
+
"Check the name against the graph, or list the loaded namespaces.",
|
|
266
|
+
{ tool: "graph_inspect", params: { uri: input } },
|
|
267
|
+
),
|
|
268
|
+
});
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
/** The boolean of an ASK result, false for anything else. */
|
|
272
|
+
function asked(result: QueryResult): boolean {
|
|
273
|
+
return result.type === "ask" && result.result;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
/** Project one endpoint, with what classifies it when it has no relations. */
|
|
277
|
+
function endpoint(
|
|
278
|
+
index: RelationIndex,
|
|
279
|
+
input: string,
|
|
280
|
+
resolved: string,
|
|
281
|
+
): ConnectEndpoint {
|
|
282
|
+
const relatable = index.adjacency.has(resolved);
|
|
283
|
+
const classifiedBy = relatable ? [] : rosterMemberships(index, resolved);
|
|
284
|
+
return {
|
|
285
|
+
input,
|
|
286
|
+
term: index.term(resolved),
|
|
287
|
+
relatable,
|
|
288
|
+
...(classifiedBy.length > 0 ? { classifiedBy } : {}),
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** The result of a successful walk: its length and every shortest path. */
|
|
293
|
+
interface Walked {
|
|
294
|
+
readonly hops: number;
|
|
295
|
+
readonly paths: ConnectStep[][];
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
/**
|
|
299
|
+
* Bounded bidirectional breadth-first search for EVERY shortest path.
|
|
300
|
+
*
|
|
301
|
+
* Both sides expand a full layer at a time, always the side with the smaller
|
|
302
|
+
* frontier. Once any node is in both balls the search stops, and that is sound
|
|
303
|
+
* rather than merely convenient: with the two balls complete to radii `ra` and
|
|
304
|
+
* `rb`, every path of length at most `ra + rb` has some node in both — so the
|
|
305
|
+
* smallest total distance over the intersection IS the shortest distance, and
|
|
306
|
+
* cannot be beaten by a path still undiscovered.
|
|
307
|
+
*
|
|
308
|
+
* Completeness follows from the same fact: for a shortest path of length L,
|
|
309
|
+
* some node on it is a meeting node whose two distances sum to L, and that
|
|
310
|
+
* node's two arrival maps hold every shortest way to reach it from either end.
|
|
311
|
+
* A path can therefore be generated more than once — via different meeting
|
|
312
|
+
* nodes — which is why the enumeration deduplicates.
|
|
313
|
+
*
|
|
314
|
+
* @param index - The session's relation index.
|
|
315
|
+
* @param a - The resolved start node.
|
|
316
|
+
* @param b - The resolved end node.
|
|
317
|
+
* @param hopLimit - The largest path length that may be returned.
|
|
318
|
+
* @returns The hop count and every distinct shortest path, or undefined.
|
|
319
|
+
*/
|
|
320
|
+
function walk(
|
|
321
|
+
index: RelationIndex,
|
|
322
|
+
a: string,
|
|
323
|
+
b: string,
|
|
324
|
+
hopLimit: number,
|
|
325
|
+
): Walked | undefined {
|
|
326
|
+
const from: Side = {
|
|
327
|
+
dist: new Map([[a, 0]]),
|
|
328
|
+
arrivals: new Map(),
|
|
329
|
+
frontier: [a],
|
|
330
|
+
radius: 0,
|
|
331
|
+
};
|
|
332
|
+
const to: Side = {
|
|
333
|
+
dist: new Map([[b, 0]]),
|
|
334
|
+
arrivals: new Map(),
|
|
335
|
+
frontier: [b],
|
|
336
|
+
radius: 0,
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
let meeting = meet(from, to);
|
|
340
|
+
while (meeting === undefined) {
|
|
341
|
+
if (from.radius + to.radius >= hopLimit) return undefined;
|
|
342
|
+
const side = cheaperSide(from, to);
|
|
343
|
+
// Both balls exhausted without meeting: the two sit in different
|
|
344
|
+
// components of the relation graph, and no hop limit would help.
|
|
345
|
+
if (side === undefined) return undefined;
|
|
346
|
+
expand(index, side);
|
|
347
|
+
meeting = meet(from, to);
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
return {
|
|
351
|
+
hops: meeting.distance,
|
|
352
|
+
paths: enumerate(index, from, to, meeting.nodes),
|
|
353
|
+
};
|
|
354
|
+
}
|
|
355
|
+
|
|
356
|
+
/**
|
|
357
|
+
* The side to expand next: the one with the smaller frontier.
|
|
358
|
+
*
|
|
359
|
+
* A side whose frontier has emptied has visited its whole component, so it can
|
|
360
|
+
* never be worth expanding again — but the OTHER side still can, and may yet
|
|
361
|
+
* reach into the exhausted ball. Aborting when either side empties would lose
|
|
362
|
+
* exactly those answers, so this only gives up when both are done.
|
|
363
|
+
*/
|
|
364
|
+
function cheaperSide(from: Side, to: Side): Side | undefined {
|
|
365
|
+
if (from.frontier.length === 0) {
|
|
366
|
+
return to.frontier.length === 0 ? undefined : to;
|
|
367
|
+
}
|
|
368
|
+
if (to.frontier.length === 0) return from;
|
|
369
|
+
return from.frontier.length <= to.frontier.length ? from : to;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** The meeting nodes at the smallest total distance, once there are any. */
|
|
373
|
+
function meet(
|
|
374
|
+
from: Side,
|
|
375
|
+
to: Side,
|
|
376
|
+
): { distance: number; nodes: string[] } | undefined {
|
|
377
|
+
const [inner, outer] =
|
|
378
|
+
from.dist.size <= to.dist.size ? [from, to] : [to, from];
|
|
379
|
+
let best = Number.POSITIVE_INFINITY;
|
|
380
|
+
let nodes: string[] = [];
|
|
381
|
+
for (const [node, near] of inner.dist) {
|
|
382
|
+
const far = outer.dist.get(node);
|
|
383
|
+
if (far === undefined) continue;
|
|
384
|
+
const total = near + far;
|
|
385
|
+
if (total < best) {
|
|
386
|
+
best = total;
|
|
387
|
+
nodes = [node];
|
|
388
|
+
} else if (total === best) nodes.push(node);
|
|
389
|
+
}
|
|
390
|
+
return nodes.length > 0 ? { distance: best, nodes } : undefined;
|
|
391
|
+
}
|
|
392
|
+
|
|
393
|
+
/**
|
|
394
|
+
* Expand one side of the search by exactly one layer.
|
|
395
|
+
*
|
|
396
|
+
* A neighbour already known at a SHALLOWER depth is not on any shortest path
|
|
397
|
+
* through this layer and is skipped; a neighbour reached again within the SAME
|
|
398
|
+
* layer records a second arrival, which is how a pair with several shortest
|
|
399
|
+
* paths keeps all of them.
|
|
400
|
+
*/
|
|
401
|
+
function expand(index: RelationIndex, side: Side): void {
|
|
402
|
+
const depth = side.radius + 1;
|
|
403
|
+
const next = new Set<string>();
|
|
404
|
+
for (const node of side.frontier) {
|
|
405
|
+
for (const edge of index.adjacency.get(node) ?? []) {
|
|
406
|
+
const known = side.dist.get(edge.to);
|
|
407
|
+
if (known !== undefined && known < depth) continue;
|
|
408
|
+
if (known === undefined) side.dist.set(edge.to, depth);
|
|
409
|
+
arrive(side, edge, node);
|
|
410
|
+
next.add(edge.to);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
side.frontier = [...next];
|
|
414
|
+
side.radius = depth;
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
/**
|
|
418
|
+
* Record how a node was reached.
|
|
419
|
+
*
|
|
420
|
+
* No duplicate check, and that is a claim about the index rather than an
|
|
421
|
+
* oversight: the scan deduplicates by triple, so one node's adjacency list
|
|
422
|
+
* holds each `(neighbour, predicate, direction)` at most once and no arrival
|
|
423
|
+
* can repeat. The check that used to be here was therefore dead — and it cost
|
|
424
|
+
* 704 ms on one measured pair, because scanning the list for every edge is
|
|
425
|
+
* quadratic in the number of parents a hub is reached from.
|
|
426
|
+
*/
|
|
427
|
+
function arrive(side: Side, edge: RelationEdge, from: string): void {
|
|
428
|
+
const arrival: Arrival = {
|
|
429
|
+
other: from,
|
|
430
|
+
predicate: edge.predicate,
|
|
431
|
+
forward: edge.forward,
|
|
432
|
+
};
|
|
433
|
+
const list = side.arrivals.get(edge.to);
|
|
434
|
+
if (list) list.push(arrival);
|
|
435
|
+
else side.arrivals.set(edge.to, [arrival]);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* Every distinct shortest path, assembled from the two arrival maps.
|
|
440
|
+
*
|
|
441
|
+
* Deduplicated by the path's own canonical form, because one path is reachable
|
|
442
|
+
* through several meeting nodes and the answer promises distinct paths.
|
|
443
|
+
*/
|
|
444
|
+
function enumerate(
|
|
445
|
+
index: RelationIndex,
|
|
446
|
+
from: Side,
|
|
447
|
+
to: Side,
|
|
448
|
+
meetingNodes: readonly string[],
|
|
449
|
+
): ConnectStep[][] {
|
|
450
|
+
const heads = new Map<string, ConnectStep[][]>();
|
|
451
|
+
const tails = new Map<string, ConnectStep[][]>();
|
|
452
|
+
const distinct = new Map<string, ConnectStep[]>();
|
|
453
|
+
|
|
454
|
+
for (const node of meetingNodes) {
|
|
455
|
+
const before = head(index, from, node, heads);
|
|
456
|
+
const after = tail(index, to, node, tails);
|
|
457
|
+
for (const start of before) {
|
|
458
|
+
for (const finish of after) {
|
|
459
|
+
if (distinct.size >= ENUMERATION_CEILING) {
|
|
460
|
+
return [...distinct.values()];
|
|
461
|
+
}
|
|
462
|
+
const steps = [...start, ...finish];
|
|
463
|
+
distinct.set(canonical(steps), steps);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
return [...distinct.values()];
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
/** Every shortest walk from the start endpoint to `node`, memoised. */
|
|
471
|
+
function head(
|
|
472
|
+
index: RelationIndex,
|
|
473
|
+
side: Side,
|
|
474
|
+
node: string,
|
|
475
|
+
memo: Map<string, ConnectStep[][]>,
|
|
476
|
+
): ConnectStep[][] {
|
|
477
|
+
const cached = memo.get(node);
|
|
478
|
+
if (cached) return cached;
|
|
479
|
+
if (side.dist.get(node) === 0) {
|
|
480
|
+
const base: ConnectStep[][] = [[]];
|
|
481
|
+
memo.set(node, base);
|
|
482
|
+
return base;
|
|
483
|
+
}
|
|
484
|
+
const paths: ConnectStep[][] = [];
|
|
485
|
+
for (const arrival of side.arrivals.get(node) ?? []) {
|
|
486
|
+
const step: ConnectStep = {
|
|
487
|
+
from: index.term(arrival.other),
|
|
488
|
+
predicate: index.term(arrival.predicate),
|
|
489
|
+
direction: arrival.forward ? "forward" : "reverse",
|
|
490
|
+
to: index.term(node),
|
|
491
|
+
};
|
|
492
|
+
for (const prefix of head(index, side, arrival.other, memo)) {
|
|
493
|
+
paths.push([...prefix, step]);
|
|
494
|
+
}
|
|
495
|
+
}
|
|
496
|
+
memo.set(node, paths);
|
|
497
|
+
return paths;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Every shortest walk from `node` to the end endpoint, memoised.
|
|
502
|
+
*
|
|
503
|
+
* The end side's arrivals record edges pointing TOWARDS `b`, so each one is
|
|
504
|
+
* traversed the other way round here — which flips the direction the step
|
|
505
|
+
* reports, and is exactly why `direction` is part of the answer.
|
|
506
|
+
*/
|
|
507
|
+
function tail(
|
|
508
|
+
index: RelationIndex,
|
|
509
|
+
side: Side,
|
|
510
|
+
node: string,
|
|
511
|
+
memo: Map<string, ConnectStep[][]>,
|
|
512
|
+
): ConnectStep[][] {
|
|
513
|
+
const cached = memo.get(node);
|
|
514
|
+
if (cached) return cached;
|
|
515
|
+
if (side.dist.get(node) === 0) {
|
|
516
|
+
const base: ConnectStep[][] = [[]];
|
|
517
|
+
memo.set(node, base);
|
|
518
|
+
return base;
|
|
519
|
+
}
|
|
520
|
+
const paths: ConnectStep[][] = [];
|
|
521
|
+
for (const arrival of side.arrivals.get(node) ?? []) {
|
|
522
|
+
const step: ConnectStep = {
|
|
523
|
+
from: index.term(node),
|
|
524
|
+
predicate: index.term(arrival.predicate),
|
|
525
|
+
direction: arrival.forward ? "reverse" : "forward",
|
|
526
|
+
to: index.term(arrival.other),
|
|
527
|
+
};
|
|
528
|
+
for (const suffix of tail(index, side, arrival.other, memo)) {
|
|
529
|
+
paths.push([step, ...suffix]);
|
|
530
|
+
}
|
|
531
|
+
}
|
|
532
|
+
memo.set(node, paths);
|
|
533
|
+
return paths;
|
|
534
|
+
}
|
|
535
|
+
|
|
536
|
+
/**
|
|
537
|
+
* A path's canonical form: the sequence of directed predicates and targets.
|
|
538
|
+
*
|
|
539
|
+
* Both the deduplication key and the final tiebreak of the ranking, so the same
|
|
540
|
+
* question asked twice gets the same answer in the same order — the entity
|
|
541
|
+
* reader sorts for the same reason.
|
|
542
|
+
*/
|
|
543
|
+
function canonical(steps: readonly ConnectStep[]): string {
|
|
544
|
+
return steps
|
|
545
|
+
.map(
|
|
546
|
+
(step) =>
|
|
547
|
+
`${step.direction === "forward" ? ">" : "<"}${step.predicate.value}${SEP}${step.to.value}`,
|
|
548
|
+
)
|
|
549
|
+
.join("|");
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
/**
|
|
553
|
+
* Rank the shortest paths: hop count, then fewest distinct predicates, then
|
|
554
|
+
* canonically.
|
|
555
|
+
*
|
|
556
|
+
* Every path here is already the same length, so the hop-count key never
|
|
557
|
+
* decides anything — it is kept because it is the FIRST key of the stated
|
|
558
|
+
* ranking, and a ranking whose stated first key is absent from its code is a
|
|
559
|
+
* ranking a reader cannot check. Fewest distinct predicates comes next because
|
|
560
|
+
* a path that stays inside one relation family explains better than one that
|
|
561
|
+
* hops vocabularies.
|
|
562
|
+
*/
|
|
563
|
+
function rank(paths: ConnectPath[]): ConnectPath[] {
|
|
564
|
+
const families = new Map<ConnectPath, number>();
|
|
565
|
+
for (const path of paths) {
|
|
566
|
+
families.set(
|
|
567
|
+
path,
|
|
568
|
+
new Set(path.steps.map((step) => step.predicate.value)).size,
|
|
569
|
+
);
|
|
570
|
+
}
|
|
571
|
+
return paths.sort(
|
|
572
|
+
(x, y) =>
|
|
573
|
+
x.steps.length - y.steps.length ||
|
|
574
|
+
(families.get(x) ?? 0) - (families.get(y) ?? 0) ||
|
|
575
|
+
canonical(x.steps).localeCompare(canonical(y.steps)),
|
|
576
|
+
);
|
|
577
|
+
}
|
|
@@ -7,11 +7,19 @@
|
|
|
7
7
|
* syntax/execution failure becomes INVALID_INPUT (exit 2) with a recovery
|
|
8
8
|
* pointer to the loaded namespaces. A `PragmaError` that already carries a
|
|
9
9
|
* precise code (e.g. STORE_UNAVAILABLE from a cold boot) is re-thrown unchanged.
|
|
10
|
+
*
|
|
11
|
+
* Between the caller and the parser sits {@link queryText}: pragma's own names
|
|
12
|
+
* are handed out in a prefixed form the SPARQL grammar cannot carry
|
|
13
|
+
* (`ds:global.component.button`, `dt:s4/web/--color-text`), so they are
|
|
14
|
+
* expanded to the IRIs they stand for BEFORE the parser sees them, and a parse
|
|
15
|
+
* failure is reported at the position in the text the CALLER wrote rather than
|
|
16
|
+
* in the prefix-prepended text only the store ever sees.
|
|
10
17
|
*/
|
|
11
18
|
|
|
12
19
|
import type { QueryResult } from "@canonical/ke";
|
|
13
20
|
import { cliRecovery, PragmaError } from "../../kernel/error/index.js";
|
|
14
21
|
import type { PragmaRuntime } from "../../kernel/runtime/index.js";
|
|
22
|
+
import { expandPrefixedNames, trimQueryError } from "./queryText.js";
|
|
15
23
|
|
|
16
24
|
/**
|
|
17
25
|
* Execute a raw SPARQL query against the booted store.
|
|
@@ -25,15 +33,22 @@ export async function runQuery(
|
|
|
25
33
|
rt: PragmaRuntime,
|
|
26
34
|
sparql: string,
|
|
27
35
|
): Promise<QueryResult> {
|
|
36
|
+
// The store's own prefix map: what the facade prepends to the query (one
|
|
37
|
+
// PREFIX line each, which is the offset a reported line number carries), and
|
|
38
|
+
// what a prefixed name in the caller's text will be resolved against.
|
|
39
|
+
const { prefixes } = await rt.store.get();
|
|
40
|
+
const query = expandPrefixedNames(sparql, prefixes);
|
|
28
41
|
try {
|
|
29
|
-
return await rt.query.sparql(
|
|
42
|
+
return await rt.query.sparql(query.text);
|
|
30
43
|
} catch (error) {
|
|
31
44
|
if (error instanceof PragmaError) throw error;
|
|
32
|
-
// Keep the parser's own message — the WHY (bad token,
|
|
33
|
-
//
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
45
|
+
// Keep the parser's own message — the WHY (bad token, an unknown prefix) —
|
|
46
|
+
// instead of an opaque "(query)" placeholder, but positioned in the
|
|
47
|
+
// caller's text and without the Unicode-range dump.
|
|
48
|
+
const raw = (error instanceof Error ? error.message : String(error)).trim();
|
|
49
|
+
const detail = raw
|
|
50
|
+
? trimQueryError(raw, Object.keys(prefixes).length, query.rewritten)
|
|
51
|
+
: "";
|
|
37
52
|
throw new PragmaError({
|
|
38
53
|
code: "INVALID_INPUT",
|
|
39
54
|
message: detail
|
|
@@ -53,6 +53,12 @@ describe("lazy dispatch — module-graph probe (PROTECTED)", () => {
|
|
|
53
53
|
// is inert data (pinned below).
|
|
54
54
|
expect(has(graph, "info/collectInfo.ts")).toBe(false);
|
|
55
55
|
expect(has(graph, "config/collectConfigShow.ts")).toBe(false);
|
|
56
|
+
// `graph connect`'s traversal AND the relation index it walks. The index is
|
|
57
|
+
// one scan of every edge in the store — a measured ~460 ms on the shipped
|
|
58
|
+
// pack — so a process that never asks a connect question must not even load
|
|
59
|
+
// the module that could build it, let alone build it.
|
|
60
|
+
expect(has(graph, "graph/runConnect.ts")).toBe(false);
|
|
61
|
+
expect(has(graph, "graph/relationIndex.ts")).toBe(false);
|
|
56
62
|
expect(has(graph, "kernel/config/readConfig.ts")).toBe(false);
|
|
57
63
|
expect(has(graph, "kernel/config/defaults.ts")).toBe(false);
|
|
58
64
|
expect(has(graph, "kernel/config/schema.ts")).toBe(false);
|
|
@@ -88,8 +94,9 @@ describe("lazy dispatch — module-graph probe (PROTECTED)", () => {
|
|
|
88
94
|
|
|
89
95
|
it("the dispatch path never reaches the embedded n-quads module (PROTECTED)", () => {
|
|
90
96
|
// `activeStories` reads the pack's carried stories from their OWN generated
|
|
91
|
-
// module rather than through `graphpack/embedded.ts`, which
|
|
92
|
-
//
|
|
97
|
+
// module rather than through `graphpack/embedded.ts`, which reaches the
|
|
98
|
+
// ~2.1 MB `pack.generated.ts` (dynamically since the manifest split, but
|
|
99
|
+
// this path must not reach it at all). Restoring that edge costs a
|
|
93
100
|
// measured +23 ms on EVERY dispatched command, and the whole test suite
|
|
94
101
|
// passes with it restored — the split is prose in three docblocks and
|
|
95
102
|
// nothing else. This is the constraint those docblocks describe.
|
|
@@ -104,6 +111,29 @@ describe("lazy dispatch — module-graph probe (PROTECTED)", () => {
|
|
|
104
111
|
}
|
|
105
112
|
});
|
|
106
113
|
|
|
114
|
+
it("`embedded.ts` itself never statically imports the payload (PROTECTED)", () => {
|
|
115
|
+
// The store-boot path DOES go through `graphpack/embedded.ts`, so the test
|
|
116
|
+
// above cannot cover it. `materializeEmbeddedPack` reads the manifest to
|
|
117
|
+
// name the cache directory and then returns early when the pack is already
|
|
118
|
+
// there — the common case — so a static import of `pack.generated.ts` would
|
|
119
|
+
// parse ~2.1 MB of n-quads to reach that return, on every boot.
|
|
120
|
+
//
|
|
121
|
+
// Restoring `import { dataNq } from "./embedded/pack.generated.js"` at
|
|
122
|
+
// module scope costs a measured ~370 ms per CLI invocation on a
|
|
123
|
+
// 30,340-triple pack, and every other test still passes. This is the only
|
|
124
|
+
// thing standing between that edit and a silent regression.
|
|
125
|
+
for (const entry of [
|
|
126
|
+
"../kernel/runtime/graphpack/embedded.ts",
|
|
127
|
+
"../kernel/runtime/loadSession.ts",
|
|
128
|
+
]) {
|
|
129
|
+
const graph = staticImportGraph(resolve(here, entry));
|
|
130
|
+
expect(has(graph, "embedded/pack.manifest.generated.ts"), entry).toBe(
|
|
131
|
+
true,
|
|
132
|
+
);
|
|
133
|
+
expect(has(graph, "embedded/pack.generated.ts"), entry).toBe(false);
|
|
134
|
+
}
|
|
135
|
+
});
|
|
136
|
+
|
|
107
137
|
it("the help path (buildProgram) imports no zod schema module", () => {
|
|
108
138
|
const graph = staticImportGraph(
|
|
109
139
|
resolve(here, "../kernel/project/cli/buildProgram.ts"),
|