Skip to content

feat(driver): add project driver v1 protocol and graph provenance - #165

Open
joeykchen wants to merge 1 commit into
goplus:mainfrom
joeykchen:feat/xgo-project-driver-spx
Open

feat(driver): add project driver v1 protocol and graph provenance#165
joeykchen wants to merge 1 commit into
goplus:mainfrom
joeykchen:feat/xgo-project-driver-spx

Conversation

@joeykchen

@joeykchen joeykchen commented Aug 20, 2026

Copy link
Copy Markdown
Contributor

Summary

Supersedes goplus/mod#164, which GitHub closed when its fork head branch was renamed.

This PR adds the goplus/mod language and contract layer for the XGo Project Driver v1 design tracked by goplus/spx#1741. It defines driver metadata, resolved graph provenance, and a driver-neutral request protocol without adding XGo dispatch or SPX-specific runtime policy.

What changed

  • Add driver <protocol> <driver-package> metadata scoped to the nearest preceding project, with strict protocol and import-path validation.
  • Expose the metadata as modfile.Project.Driver without a runtime alias or any compatibility-specific parser branch.
  • Add driverprotocol with the xgo-driver-v1 preamble, typed run/build requests, deterministic argv encoding, and DriverPackage/DriverOrigin provenance.
  • Validate graph/build policy, project and pack metadata, driver identity, application arguments, and action-specific output fields.
  • Capture SHA-256 identities for the exact go.mod and gox.mod/gop.mod snapshots consumed during resolution.
  • Preserve MVS-selected module identity separately from the effective replacement source.
  • Import caller-resolved class graphs, validate target/declaration snapshots, and attach provenance plus minimum-XGo metadata to class projects.

Compatibility and boundaries

Projects without a driver retain their existing behavior. Built-in projects remain provenance-free, and the existing class import path remains available.

The driver protocol floor and a module's general xgo minimum are independent requirements; consumers enforce whichever minimum is higher. Filesystem checks that need live driver inputs remain the dispatcher's or driver's responsibility.

Verification

GOWORK=off go test -race ./...
GOWORK=off go vet ./...
git diff --check

Release order

This is the first repository in the coordinated release order. Mod must land and be released before XGo and SPX update their github.com/goplus/mod requirements; the current published v0.21.2 does not contain driverprotocol.

Part of goplus/spx#1741.

@codecov

codecov Bot commented Aug 21, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 94.20485% with 43 lines in your changes missing coverage. Please review.
✅ Project coverage is 87.43%. Comparing base (f4300a7) to head (ad4bb1a).

Files with missing lines Patch % Lines
xgomod/classfile_provenance.go 86.40% 7 Missing and 7 partials ⚠️
xgomod/resolved_source.go 83.72% 7 Missing and 7 partials ⚠️
xgomod/resolved_identity.go 85.18% 2 Missing and 2 partials ⚠️
xgomod/resolved_import.go 85.71% 2 Missing and 2 partials ⚠️
driverprotocol/validation.go 97.29% 1 Missing and 1 partial ⚠️
xgomod/resolved.go 87.50% 1 Missing and 1 partial ⚠️
xgomod/resolved_graph.go 95.00% 1 Missing and 1 partial ⚠️
modload/module.go 96.29% 1 Missing ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main     #165      +/-   ##
==========================================
+ Coverage   81.34%   87.43%   +6.09%     
==========================================
  Files          10       21      +11     
  Lines         906     1640     +734     
==========================================
+ Hits          737     1434     +697     
- Misses        150      169      +19     
- Partials       19       37      +18     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@joeykchen
joeykchen force-pushed the feat/xgo-project-driver-spx branch from ffc0265 to f60c71c Compare August 21, 2026 06:16
@joeykchen
joeykchen marked this pull request as ready for review August 21, 2026 06:27

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Review summary

Solid, defense-minded addition. The Encode/Parse round trip is carefully symmetric, SHA-256 identity validation is thorough (length + hex + lowercase-canonical + byte re-hash), and the module-cache split-source validation is rigorous about symlinks and cache layout. Findings below are correctness/maintainability polish — none are blocking.

Notable items (see inline comments):

  • Dead if p.infos == nil guard in importClass.
  • Unreachable filepath.IsAbs && !isAbsoluteCleanPath branch in validateResolvedModuleSyntax.
  • Undocumented positional-ordering contract on the exported ResolvedClassGraph.ClassModules.

Additional (no reliable inline location):

  • README does not document the new driver directive. README.md:14 describes the internal driverprotocol package and ImportClassesResolved, but the user-facing gox.mod authoring surface added here — driver <protocol> <package>, the v[1-9][0-9]* protocol format, and the "nearest preceding project / no block form / no duplicates" scoping rules (modfile/rule.go:406-442) — is undocumented. Authors can't learn how to write the directive this PR enables. Also, README:14 ("drivers verify identity-bearing paths before use") reads as the opposite emphasis from protocol.go:18 ("Validation is structural; consumers verify identity-bearing paths") — worth aligning the two.

  • driverprotocol never verifies the declaration digest against file bytes. Request.Validate() (request_validate.go:73-80) is filesystem-free by design and only checks that declaration-sha256 is well-formed. A driver that Parses argv and then trusts request.Declaration as a verified path+digest pair is relying on a digest that this package never checks against the file (byte-vs-digest verification lives only in the separate xgomod flow). Consider documenting on Parse/Request that consumers MUST re-hash the declaration before trusting it.

  • Silent last-wins on non-driver extension collisions. In registerProject (xgomod/classfile_provenance.go:160-171), two distinct non-driver projects claiming the same extension fall through to projects[ext] = ...; infos[ext] = ... with no diagnostic, while driver-backed collisions are rejected. This matches the legacy importClass behavior so may be intentional, but the asymmetry warrants a brief comment.

  • Minor perf (low): ImportClassesResolved re-canonicalizes the target modfile at classfile_provenance.go:57 (Stat+EvalSymlinks+Abs) although graph.validate() already canonicalized and read it via validateFileIdentity. Threading the canonicalized path/digest out of validate() would remove the duplicate syscalls. Acceptable in one-shot validation code.

  • Maintainability: option names are spelled independently in Encode (argv.go), singularOptions, and commonRequiredOptions (argv_options.go). A single source of truth (name constants) would prevent a three-list drift; currently only round-trip tests catch a mismatch.

Comment thread xgomod/classfile.go Outdated
for _, w := range c.Works {
p.projs[w.Ext] = c
info := &ProjectInfo{Project: c}
if p.infos == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Dead code: if p.infos == nil guard is unreachable. Every path into importClass originates from ImportClasses, which unconditionally sets p.infos = make(...) before any importClass/importClassFrom call. The guard also gives a false impression that importClass is safe with an uninitialized receiver, yet p.projs[ext] = c below would panic if p.projs were nil in that hypothetical. Suggest removing the guard and relying on ImportClasses initialization (consistent with how p.projs is handled).

Comment thread xgomod/resolved_validation.go Outdated
return fmt.Errorf("replacement: %w", err)
}
}
if filepath.IsAbs(m.Replace.Path) && !isAbsoluteCleanPath(m.Replace.Path) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Unreachable condition. By the time execution reaches this check, the preceding if/else-if/else (the Version == "" local branch enforces isAbsoluteCleanPath, the versioned+absolute branch already returns, and the versioned+non-absolute branch leaves filepath.IsAbs false) has exhausted the cases, so filepath.IsAbs(m.Replace.Path) && !isAbsoluteCleanPath(...) is always false. Suggest removing it to avoid implying a case the earlier branches don't already cover.

Comment thread xgomod/resolved.go Outdated

// ResolvedClassGraph is XGo's resolved graph snapshot; it is not rediscovered.
type ResolvedClassGraph struct {
Target ResolvedModule

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Undocumented positional-ordering contract on ClassModules. ResolvedClassGraph.validate() requires ClassModules[i].Selected.Path == markerPaths[i], where markerPaths comes from the order of class-marked require directives in the target modfile. This makes the order of this exported slice a strict part of the API — a caller building the graph in a natural order (map iteration, sorted-by-path) will fail with "class module %d has logical path ... want marker ..." even though the set is correct. Suggest either documenting the required ordering on this field, or relaxing validate() to a set-membership check (the seenMarkers/seenModules maps are already built).

@joeykchen
joeykchen marked this pull request as draft August 21, 2026 07:40
@joeykchen
joeykchen force-pushed the feat/xgo-project-driver-spx branch from f60c71c to 00938f9 Compare August 21, 2026 10:21
@joeykchen
joeykchen marked this pull request as ready for review August 21, 2026 10:21

@fennoai fennoai Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

FennoAI Review

Solid, well-tested change. The new driverprotocol package (argv codec + validation) and xgomod resolved-module provenance layer are idiomatic Go with descriptive, namespaced error messages and strong security-minded validation (canonical paths, NUL rejection, SHA-256 digest verification, module-cache layout checks). Test coverage is extensive.

No blocking issues found. Inline comments below cover minor correctness/maintainability cleanups. A few package-level notes that don't map to a single line:

  • Duplicated option catalog (driverprotocol/argv_options.go): singularOptions and commonRequiredOptions hand-enumerate large overlapping option-name lists that must stay in sync with Encode (argv.go) and Parse. Consider centralizing the option catalog so adding a field doesn't require touching four sites.
  • Duplicated SHA-256 validation: near-identical length/hex/lowercase checks exist in both driverprotocol/validation.go (validateSHA256) and xgomod/resolved_identity.go (validateFileIdentity), with different error styles. Since driverprotocol already imports xgomod, a single shared validator would keep the rules identical.
  • Repeated canonicalization (perf, cold path): in ImportClassesResolved, the same paths (target go.mod, effective Dir) are run through canonicalPath (stat + EvalSymlinks + Abs) and read/hashed multiple times across validateFileIdentity, graph.validate(), and importResolvedModule. Threading already-derived canonical paths / read bytes through would remove redundant stat/read work. Minor since imports are infrequent.
  • README: the new user-facing driver <protocol> <package> gox.mod/gop.mod directive (added in modfile/rule.go) isn't mentioned in the README; a one-line note would improve completeness.

Comment thread xgomod/resolved_validation.go Outdated
return fmt.Errorf("replacement: %w", err)
}
}
if filepath.IsAbs(m.Replace.Path) && !isAbsoluteCleanPath(m.Replace.Path) {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This check is unreachable. The preceding if/else-if/else fully partitions the cases: the Version == "" branch already enforces isAbsoluteCleanPath(m.Replace.Path), the filepath.IsAbs branch already returns an error, and the final else handles non-absolute versioned paths. Any absolute Replace.Path reaching this line has already been validated clean, so !isAbsoluteCleanPath(...) is always false. Consider removing it, or add a comment justifying it as defensive.

Comment thread xgomod/classfile.go Outdated
for _, w := range c.Works {
p.projs[w.Ext] = c
info := &ProjectInfo{Project: c}
if p.infos == nil {

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

This lazy p.infos == nil guard is dead defensive code: the sole caller ImportClasses now initializes p.infos at line 99 before invoking importClass. Keeping both the eager init and this guard is redundant — consider dropping the guard.

Comment thread driverprotocol/argv.go
option("driver-package", request.DriverPackage),
option("selected-path", request.DriverOrigin.Selected.Path),
option("selected-version", request.DriverOrigin.Selected.Version),
option("origin-main", fmt.Sprint(request.DriverOrigin.Main)),

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

fmt.Sprint on a known bool triggers the reflection-based formatting path plus an allocation. strconv.FormatBool(request.DriverOrigin.Main) is more direct. Cosmetic.

type Action string

const (
ActionRun Action = "run"

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

ActionRun and ActionBuild are exported but lack godoc comments, unlike every other exported symbol in this package (Version1, Action, etc.). golint/revive will flag this — a one-line comment on the block or each constant would match the file's style.

import "github.com/goplus/mod/xgomod"

const (
// Version1 is the gox.mod driver protocol value.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

"the gox.mod driver protocol value" is imprecise. This constant is the v1 protocol-version string shared by the driver directive (validated by driverProtocolRE = ^v[1-9][0-9]*$) and encoded requests; it is not literally read from gox.mod by this package. Consider rewording to "the v1 driver protocol version string."

Comment thread xgomod/resolved.go
return *m.Replace == *other.Replace
}

// IsLocal reports whether the module uses filesystem source.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

Two small nits: (1) the doc says "uses filesystem source," but a versioned replacement also resolves to a filesystem Dir/GoMod (module cache) yet returns false — the intended meaning is "local (unversioned) filesystem source"; (2) m.Main || m.Replace != nil && m.Replace.Version == "" is correct given Go precedence but reads more clearly as m.Main || (m.Replace != nil && m.Replace.Version == "").

@joeykchen
joeykchen force-pushed the feat/xgo-project-driver-spx branch from 00938f9 to ad4bb1a Compare August 21, 2026 13:00
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.

1 participant