Skip to content

refactor(op): the content-addressed path is the framework's, not a provider's - #668

Merged
David-Noble-at-work merged 3 commits into
developfrom
content-path-to-op
Aug 25, 2026
Merged

David-Noble-at-work merged 3 commits into
developfrom
content-path-to-op

Conversation

@David-Noble-at-work

Copy link
Copy Markdown
Contributor

Groundwork for #662, landed separately because it stands on its own and the rest of that phase is large.

What moved

mem.Resource.SourcePath owned the formula for where content-addressed bytes live on disk:

pkg, typeName := splitTypeID(r.ResourceType())
return runtimeEnvironment.Root().NewPath(".devlore", pkg, strings.ToLower(typeName), algo, shard, hexPart)

It was never provider-specific. It derives the subdirectory from the resource's own type id, which is
why a function pack lands under .devlore/function/resource/ — not because mem put it there, but
because the formula reads whatever resource it is handed. It lived in mem because mem was the first
content-addressed resource, not because it belonged there.

Now op.ContentPath(Resource) fsroot.Path and op.ContentReader(Resource) (io.ReadCloser, error), with
mem.SourcePath and mem.Reader delegating. The mmap-backed reader and the type-id splitter moved with it.

Why op and not mem

  • .devlore is already framework territory — RecoverySite owns .devlore/recovery. mem held the
    only other production line that knew the layout.
  • The parameter type is op.Resource, so no provider has to import another to find its own content.
  • op already packs and unpacks the content section — it calls Pack() and writes the result. Knowing
    where that result lives is the same concern.

Path composition still goes through fsroot (Root().NewPath(...)), exactly as before. Nothing joins
strings by hand and fsroot needs no new capability.

One implementation note

ContentPath reads the reachability URI through the sealed base accessor, resource.resourceBase(),
rather than through the public interface. op.Resource does not declare ReachabilityURI — a gap noted in
phase 1 — and widening the public contract for one framework caller would be the wrong trade. The sealed
accessor exists for exactly this.

Why this unblocks #662

function.Resource embeds mem.Resource cross-package, the only such embed in the tree, and it has already
forced two schedule changes. The embed exists to borrow three behaviours over function's own identity —
function builds its base with reflect.TypeFor[*function.Resource]() and hands it to the embedded value.
That is a mixin, not composition.

With the formula in op, function can drop the embed and call the helpers directly, which is what #662
does next. Of mem's thirteen methods, function genuinely used SourcePath() and the Hash field —
Pack and Unpack never delegated, and sourceBytes() already opened its own mmap.

Verification

make test 103 ok / 0 fail · make vet darwin/linux/windows · test-scenario · complexity ·
gofmt -l — all clean.

@David-Noble-at-work David-Noble-at-work self-assigned this Aug 25, 2026
@David-Noble-at-work David-Noble-at-work changed the title refactor(op): the content-address path is the framework's, not a provider's refactor(op): the content-addressed path is the framework's, not a provider's Aug 25, 2026
@David-Noble-at-work

Copy link
Copy Markdown
Contributor Author

Renamed on review: ContentPath → ContentAddressedPath, ContentReader → ContentAddressedReader,
and the file to pkg/op/content_addressed_path.go.

Content lacked specificity in exactly the package where it hurts: op already has a document content
section
— 21 mentions across the tree — so ContentPath reads plausibly as "the path to the content
section," which is a different thing entirely.

Why the spelled-out form over CASPath

CAS was the other candidate and it is not obscure here — 25 uses across pkg/ and the architecture
docs, including inside op itself (digest.go, resource.go). Go's convention would also keep the casing
correct. It lost on two counts: the codebase spells content-address(ed) out 74 times against those 25,
roughly 3:1, and ContentAddressedPath reads grammatically where ContentAddressPath would be an awkward
compound noun.

Two names that were rejected for being wrong, not merely worse

  • PackPath — tempting, because op.Packer/Pack() already exist and function calls its bytes "the
    pack". But mem.Pack() reads from Reader(): the file is Pack's source, not its output.
    function.sourceBytes() does the same. The name would have described the data flowing the wrong way.
  • ArchivePath — matches mem's own "archived content" vocabulary, but hides a precondition. The
    helper returns a zero Path unless the reachability URI is <algo>:<hex>, so the content-addressing is
    part of the contract
    , not a layout detail. A name that omits it invites callers to expect it to work for
    any resource.

Verification

make test 103 ok / 0 fail · make vet darwin/linux/windows · test-scenario · gofmt -l — all clean.
No stale ContentPath or ContentReader references remain.

@David-Noble-at-work

Copy link
Copy Markdown
Contributor Author

Two fixes and two plans, closing this PR out.

1. The dead code quality-gate caught

Moving the formula to op left four symbols behind in mem:

pkg/op/provider/mem/helpers.go:226   unused: func splitTypeID
pkg/op/provider/mem/resource.go:458  unused: type resourceReader
pkg/op/provider/mem/resource.go:471  unused: func (*resourceReader).Close
pkg/op/provider/mem/resource.go:484  unused: func (*resourceReader).Read

Removed with the strings and mmap imports only they used. splitTypeID lives in helpers.go, not
resource.go where my removal looked, and the resourceReader region did not match the pattern I used, so
both survived a cleanup that reported success.

Why my local run missed it: I verified with vet and test, neither of which sees dead code, having
earlier concluded lint was unavailable locally. It was not — ./build/star lint go ./... works fine and is
the gate CI runs. That was a habit failure, not a tooling one.

2. dist keeps its stamp proof — a hole I opened in #653

dist-all carried this, three lines above the code:

"Depends on build for its stamp check, not for its binaries… build proves on the host that those
flags bind to real symbols — which is the failure that shipped unnoticed until 2026-08-16."

Then #653 taught the stamp check to skip when PLATFORM excludes the host. So
make dist PLATFORM=linux/amd64 would have shipped archives with nothing verifying the stamp bound — the
exact defect that shipped silently for months.

dist-all now forces a host build for the proof, independent of what the caller selected. Verified:
make dist PLATFORM=linux/amd64 completes with no skip line.

That was making a gate conditional to clear a red build without tracing what depended on it. The answer
was written directly above the code.

3. Two plans captured

Deliberately not here

make lint pointing at star lint go is correct and belongs to #670, not to this PR. Unifying it turns
make check red on four pre-existing cmd/writ findings (#669), and clearing those means deciding whether
a nil-guard can be removed — a bounded question, but not this PR's.

Verification

make test 103 ok / 0 fail · make vet · star lint go ./pkg/... 0 issues · gofmt -l clean ·
make dist PLATFORM=linux/amd64 completes with the stamp proof intact.

@David-Noble-at-work
David-Noble-at-work merged commit ebc7d5e into develop Aug 25, 2026
8 checks passed
@David-Noble-at-work
David-Noble-at-work deleted the content-path-to-op branch August 25, 2026 17:18
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