@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,141 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Formatters for `graph connect` — the same answer in two registers.
|
|
3
|
+
*
|
|
4
|
+
* A person gets one ROW PER STEP grouped by path, with each node's human name
|
|
5
|
+
* from the pack index, because what a reader wants from "how do these two
|
|
6
|
+
* connect" is the chain, read left to right, with the direction of every edge
|
|
7
|
+
* visible.
|
|
8
|
+
*
|
|
9
|
+
* An agent gets ONE OBJECT. Not Markdown: the answer is structured data — two
|
|
10
|
+
* endpoints, a verdict, the paths, and the policy the walk ran under — and
|
|
11
|
+
* flattening it into prose would make a caller parse back out what it already
|
|
12
|
+
* had (Constitution XI). This is also why neither format ever explains WHY two
|
|
13
|
+
* things relate: the predicates on the path are the explanation, and a sentence
|
|
14
|
+
* synthesised from them would be pragma asserting a meaning the graph does not
|
|
15
|
+
* carry.
|
|
16
|
+
*
|
|
17
|
+
* Terms arrive already compacted and named by the relation index, against the
|
|
18
|
+
* store's own merged prefix map — so these formatters carry no prefix map and
|
|
19
|
+
* re-derive nothing.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import type { Formatters } from "../../kernel/spec/index.js";
|
|
23
|
+
import type {
|
|
24
|
+
ConnectPath,
|
|
25
|
+
ConnectRefusal,
|
|
26
|
+
ConnectResult,
|
|
27
|
+
ConnectTerm,
|
|
28
|
+
SharedRoster,
|
|
29
|
+
} from "./connect.types.js";
|
|
30
|
+
|
|
31
|
+
/** The short form of a term: prefixed when there is one, else the raw value. */
|
|
32
|
+
const short = (term: ConnectTerm): string => term.prefixed ?? term.value;
|
|
33
|
+
|
|
34
|
+
/** A term as a reader should see it — short form, with its name when known. */
|
|
35
|
+
const named = (term: ConnectTerm): string =>
|
|
36
|
+
term.title ? `${short(term)} (${term.title})` : short(term);
|
|
37
|
+
|
|
38
|
+
/** A blank node has no address, and saying so beats printing a dead handle. */
|
|
39
|
+
const node = (term: ConnectTerm): string =>
|
|
40
|
+
term.addressable === false ? `[record ${term.value}]` : named(term);
|
|
41
|
+
|
|
42
|
+
/** `1 hop` / `2 hops` — a count a reader does not have to forgive. */
|
|
43
|
+
const count = (n: number, noun: string): string =>
|
|
44
|
+
`${n} ${noun}${n === 1 ? "" : "s"}`;
|
|
45
|
+
|
|
46
|
+
/** The sentence for each way of being unconnected — one per case, never shared. */
|
|
47
|
+
const REFUSALS: Readonly<Record<ConnectRefusal, string>> = {
|
|
48
|
+
"endpoint-carries-no-relation":
|
|
49
|
+
"Not connectable: one endpoint carries no relation the model uses — it is only classified.",
|
|
50
|
+
"shares-only-rosters":
|
|
51
|
+
"Not connected in the model. The two share membership, which is not a relation.",
|
|
52
|
+
"no-path-within-hop-limit":
|
|
53
|
+
"Not connected: no relation path between them within the hop limit.",
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
/** One roster group, as a line a reader can act on. */
|
|
57
|
+
function rosterLine(roster: SharedRoster): string {
|
|
58
|
+
const shape =
|
|
59
|
+
roster.side === "subjects"
|
|
60
|
+
? `${short(roster.predicate)} -> ${node(roster.node)}`
|
|
61
|
+
: `${node(roster.node)} -> ${short(roster.predicate)}`;
|
|
62
|
+
return ` ${shape} (${roster.members} members)`;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/** One path, as a header plus one row per step. */
|
|
66
|
+
function pathLines(path: ConnectPath, ordinal: number): string[] {
|
|
67
|
+
const lines = [` Path ${ordinal} (${count(path.steps.length, "hop")})`];
|
|
68
|
+
for (const step of path.steps) {
|
|
69
|
+
const arrow =
|
|
70
|
+
step.direction === "forward"
|
|
71
|
+
? `--${short(step.predicate)}->`
|
|
72
|
+
: `<-${short(step.predicate)}--`;
|
|
73
|
+
lines.push(` ${node(step.from)} ${arrow} ${node(step.to)}`);
|
|
74
|
+
}
|
|
75
|
+
return lines;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/** The policy block every answer carries, positive or negative. */
|
|
79
|
+
function policyLines(data: ConnectResult): string[] {
|
|
80
|
+
return [
|
|
81
|
+
"",
|
|
82
|
+
` Hop limit ${data.hopLimit}; edges whose fan-in exceeds ${data.rosterThreshold} are rosters, not relations.`,
|
|
83
|
+
` Packs searched: ${data.packs.length > 0 ? data.packs.join(", ") : "none recorded"}`,
|
|
84
|
+
];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
export const connectFormatters: Formatters<ConnectResult> = {
|
|
88
|
+
plain(data) {
|
|
89
|
+
const title = `${node(data.a.term)} <-> ${node(data.b.term)}`;
|
|
90
|
+
const lines = [title, "=".repeat(Math.min(Math.max(title.length, 24), 96))];
|
|
91
|
+
|
|
92
|
+
if (data.connected) {
|
|
93
|
+
lines.push(
|
|
94
|
+
` Connected in ${count(data.hops ?? 0, "relation hop")}, by ${count(data.pathCount, "shortest path")}.`,
|
|
95
|
+
"",
|
|
96
|
+
);
|
|
97
|
+
for (const [index, path] of data.paths.entries()) {
|
|
98
|
+
lines.push(...pathLines(path, index + 1));
|
|
99
|
+
lines.push("");
|
|
100
|
+
}
|
|
101
|
+
if (data.truncated) {
|
|
102
|
+
lines.push(
|
|
103
|
+
` Showing ${data.paths.length} of ${data.pathCount} shortest paths — raise --paths for the rest.`,
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
} else {
|
|
107
|
+
lines.push(
|
|
108
|
+
` ${REFUSALS[data.unconnectedBecause ?? "no-path-within-hop-limit"]}`,
|
|
109
|
+
);
|
|
110
|
+
for (const endpoint of [data.a, data.b]) {
|
|
111
|
+
if (endpoint.relatable) continue;
|
|
112
|
+
lines.push(
|
|
113
|
+
"",
|
|
114
|
+
` ${node(endpoint.term)} carries no relation. It is classified by:`,
|
|
115
|
+
...(endpoint.classifiedBy ?? []).map(rosterLine),
|
|
116
|
+
);
|
|
117
|
+
}
|
|
118
|
+
if (data.sharedRosters.length > 0) {
|
|
119
|
+
lines.push("", " Rosters they both belong to:");
|
|
120
|
+
lines.push(...data.sharedRosters.map(rosterLine));
|
|
121
|
+
lines.push(
|
|
122
|
+
"",
|
|
123
|
+
" A roster is answered by the noun's own list verb, not by a path.",
|
|
124
|
+
);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
lines.push(...policyLines(data));
|
|
129
|
+
return lines.join("\n").trimEnd();
|
|
130
|
+
},
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* The agent-facing format IS the object — one JSON value on one line.
|
|
134
|
+
*
|
|
135
|
+
* `json` pretty-prints the same payload for a human reading a pipe; the two
|
|
136
|
+
* are the same data by construction, so an agent and a reader never see
|
|
137
|
+
* different answers to the same question.
|
|
138
|
+
*/
|
|
139
|
+
llm: (data) => JSON.stringify(data),
|
|
140
|
+
json: (data) => JSON.stringify(data, null, 2),
|
|
141
|
+
};
|
|
@@ -0,0 +1,370 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `graph connect` against the pack this CLI actually ships (PROTECTED).
|
|
3
|
+
*
|
|
4
|
+
* Nothing here pins a number the graph produces, and nothing snapshots an
|
|
5
|
+
* answer. That is deliberate, and it is the whole design of this file. This
|
|
6
|
+
* graph is about to change: new channel symbols will become addressable and
|
|
7
|
+
* hundreds of binding records will be written, and every one of those moves a
|
|
8
|
+
* hop count and a path set. A test that pinned "button is exactly 2 hops from
|
|
9
|
+
* `color.text`" would go red on a correct change, and a snapshot of an answer's
|
|
10
|
+
* bytes would go red on a formatting fix. Both teach a maintainer to update the
|
|
11
|
+
* fixture rather than to think.
|
|
12
|
+
*
|
|
13
|
+
* So what is asserted here are INVARIANTS — properties that must hold whatever
|
|
14
|
+
* the data says:
|
|
15
|
+
*
|
|
16
|
+
* - the relation rule holds over the verb's OWN output: no returned step is an
|
|
17
|
+
* edge the rule would have refused;
|
|
18
|
+
* - every returned path is the same length, and that length is minimal;
|
|
19
|
+
* - the returned paths are distinct, and two runs agree on their order;
|
|
20
|
+
* - every step is an edge the store actually asserts, in the direction claimed,
|
|
21
|
+
* and the steps join end to end from one endpoint to the other;
|
|
22
|
+
* - the limit and threshold reported are the ones applied;
|
|
23
|
+
* - the packs reported are the packs the session loaded;
|
|
24
|
+
* - asking the question the other way round returns the same paths mirrored.
|
|
25
|
+
*
|
|
26
|
+
* Every check asks the STORE, never the verb's own index — an answer checked
|
|
27
|
+
* against the structure that produced it only proves the verb agrees with
|
|
28
|
+
* itself. The questions are targeted at the nodes on the answer rather than
|
|
29
|
+
* re-scanning the graph, which is both cheaper and a more direct question.
|
|
30
|
+
*
|
|
31
|
+
* Exact numbers live in `connect.test.ts`, over a fixture this repo authors.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { afterAll, beforeAll, describe, expect, it } from "vitest";
|
|
35
|
+
import { verbKey } from "../../kernel/packs/uniqueness.js";
|
|
36
|
+
import { bootRuntime } from "../../kernel/runtime/boot.js";
|
|
37
|
+
import { ROSTER_THRESHOLD } from "../../kernel/runtime/readEntity.js";
|
|
38
|
+
import type {
|
|
39
|
+
PragmaRuntime,
|
|
40
|
+
StoreSession,
|
|
41
|
+
} from "../../kernel/runtime/types.js";
|
|
42
|
+
import type { VerbSpec } from "../../kernel/spec/types.js";
|
|
43
|
+
import { TEST_FLAGS } from "../../testing/helpers/projectCli.js";
|
|
44
|
+
import type { ConnectPath, ConnectResult } from "./connect.types.js";
|
|
45
|
+
import { graphModule } from "./index.js";
|
|
46
|
+
|
|
47
|
+
const connect = graphModule.verbs.find(
|
|
48
|
+
(v) => verbKey(v.path) === "graph connect",
|
|
49
|
+
) as VerbSpec;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* A pair that is connected on the shipped pack. Asserted to ANSWER, never to
|
|
53
|
+
* answer with a particular number — the block subscribes to a modifier family
|
|
54
|
+
* and that family rebinds the symbol, and if that route is ever rewritten the
|
|
55
|
+
* properties below still describe a correct answer.
|
|
56
|
+
*/
|
|
57
|
+
const CONNECTED_PAIR = ["ds:global.component.button", "dt:color.text"] as const;
|
|
58
|
+
|
|
59
|
+
/** One store for the file — booting per case triples the cost for no coverage. */
|
|
60
|
+
let rt: PragmaRuntime;
|
|
61
|
+
let session: StoreSession;
|
|
62
|
+
|
|
63
|
+
beforeAll(async () => {
|
|
64
|
+
rt = bootRuntime(TEST_FLAGS);
|
|
65
|
+
session = await rt.store.get();
|
|
66
|
+
});
|
|
67
|
+
afterAll(() => {
|
|
68
|
+
session.store.dispose();
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
/** Ask one connect question through the verb, as a projector would. */
|
|
72
|
+
async function ask(
|
|
73
|
+
a: string,
|
|
74
|
+
b: string,
|
|
75
|
+
options: { hops?: number; paths?: number } = {},
|
|
76
|
+
): Promise<ConnectResult> {
|
|
77
|
+
return (await connect.run({ a, b, ...options }, rt)) as ConnectResult;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
/** One step of a path, as the answer reports it. */
|
|
81
|
+
type Step = ConnectPath["steps"][number];
|
|
82
|
+
|
|
83
|
+
/** The triple a step claims, as subject, predicate, object. */
|
|
84
|
+
function claimed(step: Step): {
|
|
85
|
+
subject: Step["from"];
|
|
86
|
+
predicate: string;
|
|
87
|
+
object: Step["to"];
|
|
88
|
+
} {
|
|
89
|
+
const forward = step.direction === "forward";
|
|
90
|
+
return {
|
|
91
|
+
subject: forward ? step.from : step.to,
|
|
92
|
+
predicate: step.predicate.value,
|
|
93
|
+
object: forward ? step.to : step.from,
|
|
94
|
+
};
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Whether a term can be named in a query.
|
|
99
|
+
*
|
|
100
|
+
* A blank node cannot: its label is store-local and is not a legal IRI, which
|
|
101
|
+
* is the very fact that rules out a per-layer SPARQL frontier and makes the
|
|
102
|
+
* in-code walk necessary. A step with a blank node at one end is therefore
|
|
103
|
+
* checked from its other end, and one blank at BOTH ends cannot be checked from
|
|
104
|
+
* outside the index at all — recorded here rather than left as a silent pass.
|
|
105
|
+
*/
|
|
106
|
+
const nameable = (term: { addressable?: false }): boolean =>
|
|
107
|
+
term.addressable !== false;
|
|
108
|
+
|
|
109
|
+
/** The number a `?n` counting query returns. */
|
|
110
|
+
async function count(query: string): Promise<number> {
|
|
111
|
+
const result = await rt.query.sparql(query);
|
|
112
|
+
if (result.type !== "select") return 0;
|
|
113
|
+
const value = result.termBindings[0]?.n?.value;
|
|
114
|
+
return value === undefined ? 0 : Number(value);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
describe("graph connect over the shipped pack — invariants (PROTECTED)", () => {
|
|
118
|
+
it("returns no step the relation rule would have refused", async () => {
|
|
119
|
+
// The rule enforced against the verb's own output, from group sizes counted
|
|
120
|
+
// by the STORE rather than taken from the index that produced the paths.
|
|
121
|
+
const result = await ask(...CONNECTED_PAIR, { paths: 99 });
|
|
122
|
+
expect(result.connected).toBe(true);
|
|
123
|
+
let checked = 0;
|
|
124
|
+
for (const path of result.paths) {
|
|
125
|
+
for (const step of path.steps) {
|
|
126
|
+
const { subject, predicate, object } = claimed(step);
|
|
127
|
+
if (nameable(object)) {
|
|
128
|
+
const subjectsSharing = await count(
|
|
129
|
+
`SELECT (COUNT(*) AS ?n) WHERE { ?s <${predicate}> <${object.value}> }`,
|
|
130
|
+
);
|
|
131
|
+
expect(
|
|
132
|
+
subjectsSharing,
|
|
133
|
+
`(${predicate}, ${object.value}) is a roster of subjects, so this edge is not walkable`,
|
|
134
|
+
).toBeLessThanOrEqual(ROSTER_THRESHOLD);
|
|
135
|
+
checked++;
|
|
136
|
+
}
|
|
137
|
+
if (nameable(subject)) {
|
|
138
|
+
const objectsSharing = await count(
|
|
139
|
+
`SELECT (COUNT(*) AS ?n) WHERE { <${subject.value}> <${predicate}> ?o . FILTER(!isLiteral(?o)) }`,
|
|
140
|
+
);
|
|
141
|
+
expect(
|
|
142
|
+
objectsSharing,
|
|
143
|
+
`(${subject.value}, ${predicate}) is a roster of objects, so this edge is not walkable`,
|
|
144
|
+
).toBeLessThanOrEqual(ROSTER_THRESHOLD);
|
|
145
|
+
checked++;
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
expect(checked, "no group was actually checked").toBeGreaterThan(0);
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
it("walks only edges the store asserts, in the direction it asserts them", async () => {
|
|
153
|
+
const result = await ask(...CONNECTED_PAIR, { paths: 99 });
|
|
154
|
+
let checked = 0;
|
|
155
|
+
for (const path of result.paths) {
|
|
156
|
+
for (const step of path.steps) {
|
|
157
|
+
const { subject, predicate, object } = claimed(step);
|
|
158
|
+
// Asked from whichever end can be named, and the OTHER end's value is
|
|
159
|
+
// read back out of the store — so a blank node is verified too, by the
|
|
160
|
+
// label the store itself hands back.
|
|
161
|
+
if (nameable(subject)) {
|
|
162
|
+
const rows = await rt.query.sparql(
|
|
163
|
+
`SELECT ?o WHERE { <${subject.value}> <${predicate}> ?o }`,
|
|
164
|
+
);
|
|
165
|
+
const objects =
|
|
166
|
+
rows.type === "select"
|
|
167
|
+
? rows.termBindings.map((binding) => binding.o?.value)
|
|
168
|
+
: [];
|
|
169
|
+
expect(
|
|
170
|
+
objects,
|
|
171
|
+
`the store asserts no <${subject.value}> <${predicate}> <${object.value}>`,
|
|
172
|
+
).toContain(object.value);
|
|
173
|
+
checked++;
|
|
174
|
+
} else if (nameable(object)) {
|
|
175
|
+
const rows = await rt.query.sparql(
|
|
176
|
+
`SELECT ?s WHERE { ?s <${predicate}> <${object.value}> }`,
|
|
177
|
+
);
|
|
178
|
+
const subjects =
|
|
179
|
+
rows.type === "select"
|
|
180
|
+
? rows.termBindings.map((binding) => binding.s?.value)
|
|
181
|
+
: [];
|
|
182
|
+
expect(subjects).toContain(subject.value);
|
|
183
|
+
checked++;
|
|
184
|
+
}
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
expect(checked, "no step was actually checked").toBeGreaterThan(0);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("returns paths that actually join one endpoint to the other", async () => {
|
|
191
|
+
const result = await ask(...CONNECTED_PAIR, { paths: 99 });
|
|
192
|
+
for (const path of result.paths) {
|
|
193
|
+
expect(path.steps[0]?.from.value).toBe(result.a.term.value);
|
|
194
|
+
expect(path.steps.at(-1)?.to.value).toBe(result.b.term.value);
|
|
195
|
+
// Each step starts where the previous one ended: a list of edges that
|
|
196
|
+
// does not join up is not a path.
|
|
197
|
+
for (const [index, step] of path.steps.entries()) {
|
|
198
|
+
if (index === 0) continue;
|
|
199
|
+
expect(step.from.value).toBe(path.steps[index - 1]?.to.value);
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
it("returns SHORTEST paths only — all of one length, and that length minimal", async () => {
|
|
205
|
+
const result = await ask(...CONNECTED_PAIR, { paths: 99 });
|
|
206
|
+
const lengths = new Set(result.paths.map((path) => path.steps.length));
|
|
207
|
+
expect(lengths.size).toBe(1);
|
|
208
|
+
expect([...lengths][0]).toBe(result.hops);
|
|
209
|
+
// Minimal, proved against the verb itself: one hop short of the reported
|
|
210
|
+
// distance, the same question has no answer. Only askable when there IS a
|
|
211
|
+
// tighter limit — a one-hop answer is minimal by construction, and the
|
|
212
|
+
// limit's own floor is 1.
|
|
213
|
+
const hops = result.hops ?? 0;
|
|
214
|
+
expect(hops).toBeGreaterThan(0);
|
|
215
|
+
if (hops > 1) {
|
|
216
|
+
const tighter = await ask(...CONNECTED_PAIR, { hops: hops - 1 });
|
|
217
|
+
expect(tighter.connected).toBe(false);
|
|
218
|
+
}
|
|
219
|
+
});
|
|
220
|
+
|
|
221
|
+
it("returns distinct paths, in an order two runs agree on", async () => {
|
|
222
|
+
const once = await ask(...CONNECTED_PAIR, { paths: 99 });
|
|
223
|
+
const twice = await ask(...CONNECTED_PAIR, { paths: 99 });
|
|
224
|
+
const key = (path: ConnectPath): string =>
|
|
225
|
+
path.steps
|
|
226
|
+
.map(
|
|
227
|
+
(step) =>
|
|
228
|
+
`${step.direction}:${step.predicate.value}:${step.to.value}`,
|
|
229
|
+
)
|
|
230
|
+
.join("|");
|
|
231
|
+
const keys = once.paths.map(key);
|
|
232
|
+
expect(new Set(keys).size, "duplicate paths in one answer").toBe(
|
|
233
|
+
keys.length,
|
|
234
|
+
);
|
|
235
|
+
expect(twice.paths.map(key)).toEqual(keys);
|
|
236
|
+
// Deterministic BY THE STATED KEY: hop count, then fewest distinct
|
|
237
|
+
// predicates, then the path's canonical form.
|
|
238
|
+
const families = once.paths.map(
|
|
239
|
+
(path) => new Set(path.steps.map((step) => step.predicate.value)).size,
|
|
240
|
+
);
|
|
241
|
+
expect([...families]).toEqual([...families].sort((x, y) => x - y));
|
|
242
|
+
});
|
|
243
|
+
|
|
244
|
+
it("reports the hop limit and roster threshold it actually applied", async () => {
|
|
245
|
+
const raised = await ask(...CONNECTED_PAIR, { hops: 6 });
|
|
246
|
+
expect(raised.hopLimit).toBe(6);
|
|
247
|
+
expect(raised.rosterThreshold).toBe(ROSTER_THRESHOLD);
|
|
248
|
+
const tight = await ask(...CONNECTED_PAIR, { hops: 1 });
|
|
249
|
+
expect(tight.hopLimit).toBe(1);
|
|
250
|
+
// The limit is not decoration: a path longer than it is not returned.
|
|
251
|
+
for (const path of tight.paths) {
|
|
252
|
+
expect(path.steps.length).toBeLessThanOrEqual(1);
|
|
253
|
+
}
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
it("names the packs the session it answered from was built with", async () => {
|
|
257
|
+
const result = await ask(...CONNECTED_PAIR);
|
|
258
|
+
expect(result.packs).toEqual(
|
|
259
|
+
session.manifest.sourceRef
|
|
260
|
+
.split(",")
|
|
261
|
+
.map((ref) => ref.trim())
|
|
262
|
+
.filter((ref) => ref.length > 0),
|
|
263
|
+
);
|
|
264
|
+
expect(result.packs.length).toBeGreaterThan(0);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it("answers the same path set whichever endpoint is asked first", async () => {
|
|
268
|
+
const forwards = await ask(...CONNECTED_PAIR, { paths: 99 });
|
|
269
|
+
const backwards = await ask(CONNECTED_PAIR[1], CONNECTED_PAIR[0], {
|
|
270
|
+
paths: 99,
|
|
271
|
+
});
|
|
272
|
+
expect(backwards.hops).toBe(forwards.hops);
|
|
273
|
+
expect(backwards.pathCount).toBe(forwards.pathCount);
|
|
274
|
+
// Modulo direction: reverse the order of the steps, swap each step's ends,
|
|
275
|
+
// and flip the direction it claims.
|
|
276
|
+
const mirror = (result: ConnectResult): string[] =>
|
|
277
|
+
result.paths
|
|
278
|
+
.map((path) =>
|
|
279
|
+
[...path.steps]
|
|
280
|
+
.reverse()
|
|
281
|
+
.map(
|
|
282
|
+
(step) =>
|
|
283
|
+
`${step.direction === "forward" ? "reverse" : "forward"}:${step.predicate.value}:${step.from.value}`,
|
|
284
|
+
)
|
|
285
|
+
.join("|"),
|
|
286
|
+
)
|
|
287
|
+
.sort();
|
|
288
|
+
const plain = (result: ConnectResult): string[] =>
|
|
289
|
+
result.paths
|
|
290
|
+
.map((path) =>
|
|
291
|
+
path.steps
|
|
292
|
+
.map(
|
|
293
|
+
(step) =>
|
|
294
|
+
`${step.direction}:${step.predicate.value}:${step.to.value}`,
|
|
295
|
+
)
|
|
296
|
+
.join("|"),
|
|
297
|
+
)
|
|
298
|
+
.sort();
|
|
299
|
+
expect(mirror(backwards)).toEqual(plain(forwards));
|
|
300
|
+
});
|
|
301
|
+
|
|
302
|
+
it("answers a real pair in the shape the contract promises", async () => {
|
|
303
|
+
// A SHAPE assertion, not a value one: it parses, it carries at least one
|
|
304
|
+
// path, and every step names a predicate and a direction — without saying
|
|
305
|
+
// which predicate or which direction, because that is the graph's business
|
|
306
|
+
// and the graph is about to change.
|
|
307
|
+
const result = await ask(...CONNECTED_PAIR);
|
|
308
|
+
const round = JSON.parse(
|
|
309
|
+
connect.output.formatters.json?.(result) ?? "null",
|
|
310
|
+
) as ConnectResult;
|
|
311
|
+
expect(round.connected).toBe(true);
|
|
312
|
+
expect(round.paths.length).toBeGreaterThan(0);
|
|
313
|
+
for (const path of round.paths) {
|
|
314
|
+
expect(path.steps.length).toBeGreaterThan(0);
|
|
315
|
+
for (const step of path.steps) {
|
|
316
|
+
expect(step.predicate.value).toMatch(/^\w+:/);
|
|
317
|
+
expect(["forward", "reverse"]).toContain(step.direction);
|
|
318
|
+
expect(step.from.value.length).toBeGreaterThan(0);
|
|
319
|
+
expect(step.to.value.length).toBeGreaterThan(0);
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
|
|
324
|
+
it("runs every documented example as WRITTEN, and each one answers", async () => {
|
|
325
|
+
// The promise an example makes to a stranger. It lives here rather than in
|
|
326
|
+
// `examples.exec.test.ts` because that file's cases are already the most
|
|
327
|
+
// contended in the package and this one needs the same shipped store this
|
|
328
|
+
// file has already booted — one store per concern, not two.
|
|
329
|
+
//
|
|
330
|
+
// That it ANSWERS is what is checked. An endpoint that is not in the graph
|
|
331
|
+
// THROWS, which is exactly the broken example worth catching. Whether a
|
|
332
|
+
// pair turns out to be connected is the graph's business and deliberately
|
|
333
|
+
// not pinned: new symbols and binding records will move that, and an
|
|
334
|
+
// example is not wrong for demonstrating a real answer that changed.
|
|
335
|
+
expect(connect.examples?.length).toBeGreaterThan(0);
|
|
336
|
+
const positionals = connect.params
|
|
337
|
+
.filter((param) => param.positional)
|
|
338
|
+
.map((param) => param.name);
|
|
339
|
+
expect(positionals).toHaveLength(2);
|
|
340
|
+
|
|
341
|
+
for (const [index, example] of (connect.examples ?? []).entries()) {
|
|
342
|
+
// Both endpoints, read from the example's own `cmd` rather than retyped:
|
|
343
|
+
// the tokens after the verb, up to the first flag.
|
|
344
|
+
const endpoints: string[] = [];
|
|
345
|
+
for (const token of example.cmd.trim().split(/\s+/).slice(3)) {
|
|
346
|
+
if (token.startsWith("--")) break;
|
|
347
|
+
endpoints.push(token);
|
|
348
|
+
}
|
|
349
|
+
expect(endpoints, `example ${index} names two endpoints`).toHaveLength(2);
|
|
350
|
+
const result = await ask(endpoints[0] as string, endpoints[1] as string);
|
|
351
|
+
// Coherent with itself, whichever answer it gave.
|
|
352
|
+
expect(result.connected).toBe(result.paths.length > 0);
|
|
353
|
+
expect(result.connected).toBe(result.hops !== null);
|
|
354
|
+
expect(result.connected).toBe(result.unconnectedBecause === undefined);
|
|
355
|
+
expect(result.packs.length).toBeGreaterThan(0);
|
|
356
|
+
}
|
|
357
|
+
});
|
|
358
|
+
|
|
359
|
+
it("builds its index once per session and reuses it", async () => {
|
|
360
|
+
const { relationIndex } = await import("./relationIndex.js");
|
|
361
|
+
const first = await relationIndex({ session, sparql: rt.query.sparql });
|
|
362
|
+
const second = await relationIndex({ session, sparql: rt.query.sparql });
|
|
363
|
+
// The SAME object, not an equal one: a second scan of a 33k-edge graph per
|
|
364
|
+
// question is the cost this memo exists to avoid.
|
|
365
|
+
expect(second).toBe(first);
|
|
366
|
+
expect(first.relationEdges).toBeGreaterThan(0);
|
|
367
|
+
expect(first.relationEdges).toBeLessThan(first.scannedEdges);
|
|
368
|
+
expect(first.relationNodes).toBeGreaterThan(0);
|
|
369
|
+
});
|
|
370
|
+
});
|