Skip to content

Add heuristic resolver, Chain, and case-insensitive matching - #1

Merged
andrew merged 2 commits into
mainfrom
heuristic-resolver
Aug 11, 2026
Merged

Add heuristic resolver, Chain, and case-insensitive matching#1
andrew merged 2 commits into
mainfrom
heuristic-resolver

Conversation

@andrew

@andrew andrew commented Aug 11, 2026

Copy link
Copy Markdown
Contributor

heuristic.Resolver() derives conventional source names from a package's PURL type and name alone: npm wsws (+ any ws/... subpath), PyPI Engine-IO-Parserengine_io_parser, gem active_support → feature active_support and constant ActiveSupport, cargo tokio-utiltokio_util, composer vendor → PSR-4 root, hex → camelised module, maven → group-id package prefix. Every returned name carries EvidenceHeuristic. Ecosystems without a convention resolve to an empty surface.

Chain(resolvers...) runs several SurfaceResolvers over the same package and merges results via MergeSurfaceResults, so a curated catalog or artifact resolver can be tried first and the naming convention fills what it does not cover. A resolver error becomes a diagnostic and the chain continues.

ProvidedName.CaseInsensitive folds ASCII case during matching. PHP namespace resolution is case-insensitive at the language level (use GuzzleHttp\Client and use guzzlehttp\client resolve identically), so a heuristic composer→PSR-4 mapping cannot produce one canonical spelling; the composer convention sets this flag. Name still retains its canonical spelling.

SurfaceResolverFunc adapts a plain function to the interface; Chain and heuristic.Resolver return the concrete type.

The field addition is backward-compatible; existing struct literals and merge/dedup keys carry the new field through.

heuristic.Resolver derives conventional source names from a package's
PURL type and name for npm, pypi, golang, gem, cargo, composer, hex,
and maven, tagged with EvidenceHeuristic. It reads no files and makes
no network requests.

Chain runs several SurfaceResolvers over the same package and merges
their results, so a curated catalog or artifact resolver can be tried
first and the naming convention fills whatever it does not cover.

ProvidedName.CaseInsensitive folds ASCII case during matching for
languages whose module or namespace lookup is itself case-insensitive.
The composer heuristic sets it since PHP resolves 'use GuzzleHttp\X'
and 'use guzzlehttp\x' identically and no naming rule can recover the
canonical spelling from the vendor name alone.

SurfaceResolverFunc adapts a plain function to a SurfaceResolver.

Copilot AI left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR expands surface-resolution capabilities by adding a heuristic resolver (PURL → conventional source names), a composable Chain resolver to merge multiple resolution strategies, and a ProvidedName.CaseInsensitive flag to support languages with case-insensitive name lookup (notably PHP namespaces).

Changes:

  • Add ProvidedName.CaseInsensitive and apply it in name matching plus merge/dedup keys.
  • Introduce SurfaceResolverFunc and Chain(resolvers...) to compose multiple SurfaceResolvers and merge their results.
  • Add heuristic.Resolver() with per-ecosystem naming conventions and accompanying tests/docs.

Reviewed changes

Copilot reviewed 10 out of 10 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
types.go Adds ProvidedName.CaseInsensitive to support case-folded matching semantics.
match.go Implements case-insensitive matching behavior in ProvidedName.Matches.
import.go Includes CaseInsensitive in import match deduplication keys.
merge.go Includes CaseInsensitive in provided-name merge keys (but see determinism note in review comments).
resolver.go Adds SurfaceResolverFunc and Chain to combine multiple surface resolvers.
resolver_test.go Adds tests validating chaining behavior (merge, dedupe, error handling, nil/empty).
heuristic/heuristic.go Adds heuristic resolver mapping PURL types to conventional provided names.
heuristic/heuristic_test.go Adds tests for heuristic conventions and evidence/diagnostic behavior.
match_test.go Adds tests for CaseInsensitive matching.
README.md Documents case-insensitive matching, heuristic surfaces, and chaining.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Comment thread heuristic/heuristic.go
Comment thread resolver.go
Comment thread merge.go
…eterminism

The PyPI heuristic comment referenced PEP 503 equivalence of -/_/., but
the code only maps hyphens to underscores. That is intentional: dotted
distribution names conventionally install as namespace packages with the
dot preserved (zope.interface, ruamel.yaml). The comment now states what
the code does and a test covers the dotted case.

Chain collected each resolver's diagnostics and also passed the full
SurfaceResult (including diagnostics) to MergeSurfaceResults, which
merged them again. mergeDiagnostics deduplicates so the output was
correct, but the work was redundant. Chain now passes only the Surface
to the merge and adds diagnostics once.

MergeSurfaceResults' sort comparator did not include CaseInsensitive, so
two entries differing only in that field could order nondeterministically
via map iteration. The comparator now falls through to CaseInsensitive
after Separator, sorting the case-sensitive entry first.
@andrew
andrew merged commit 7732160 into main Aug 11, 2026
2 checks passed
andrew added a commit to alpha-omega-security/hyrum that referenced this pull request Aug 11, 2026
One tree-sitter-backed indexer replaces the per-ecosystem regex and
text scanners in js.go, python.go, and generic.go. For each source
file it calls outline.Imports to extract import statements, filters
them via provides.ProvidedName.Matches (prefix + separator per
ecosystem, case-folded for PHP), collects the local identifiers each
matching import binds, then calls outline.Refs to trace direct member
accesses on those identifiers. Member accesses now record under the
export or module name rather than whatever local alias the target
chose, so 'import {X as Y}; Y.foo' surfaces as X.foo.

Seed receivers cover ecosystems where the dependency's canonical name
is referenced without an import line: Ruby's Bundler-autoloaded
constant, Rust and Elixir's module identifier, PHP's PSR-4 root, and
Go's implicit package name for an unaliased import.

outline is pinned to a475607 (git-pkgs/outline#30, merged) for Go
qualified_type refs and the Ruby call-with-block method-field fix;
provides to 29fb3d0 (git-pkgs/provides#1) for CaseInsensitive matching.

The strict linter set (gocognit, goconst, gocyclo, maintidx, dupl,
mnd, unparam, ireturn) is now enabled in .golangci.yml so CI enforces
what the previous commit cleaned up; the deleted files were the last
offenders.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants