Skip to content

feat(generate): in-memory validate seam + provider resolution (WS1 seams) - #2772

Merged
devarismeroxa merged 2 commits into
mainfrom
feat/ws1-generate-seams
Aug 5, 2026
Merged

feat(generate): in-memory validate seam + provider resolution (WS1 seams)#2772
devarismeroxa merged 2 commits into
mainfrom
feat/ws1-generate-seams

Conversation

@devarismeroxa

Copy link
Copy Markdown
Contributor

Two prerequisites the conduit generate design doc names before the command itself can be built. Both are fully testable without any LLM, which is why they come first.

1. validate.RunBytes — the in-memory seam (§3, "the disk seam")

generate must gate its candidate through this engine, not a second validator, and the candidate exists only in memory. validate.Run takes a path and resolves from disk, so today a candidate could only be validated by writing it out first — meaning a config that fails validation still touched the filesystem, and a disk error sat in the path of a purely in-memory operation.

validateFile already opened the path into an io.Reader and did nothing else with the path, so both paths now share validateReader rather than the in-memory path re-implementing (and drifting from) the parse → enrich → validate ordering.

RunBytes returns no error, unlike Run: Run's only hard failure is resolving path, and there is no path. Everything else comes back as a Finding, exactly as it does for a file that exists but is unparseable.

2. provider — the LLM seam and its resolution rules (§1)

A two-method interface plus deterministic selection. No new dependencies — the adapters (landing next) reuse the already-vendored go-openai client and follow the ollama processor's existing hand-rolled net/http pattern.

The load-bearing rule: there is no default vendor. Conduit's broker-neutrality principle extends to model vendors, so with several configured and no explicit selection, Resolve refuses rather than picking. A silent pick is hidden favoritism, and it makes a real support question — "why did it use provider X?" — return a wrong answer every time the ordering changes.

Two seams keep resolution off real process state and off the network:

Seam Why
Env Tests never mutate real env vars, so they stay parallel-safe and can't leak into each other
Probe Reachability without network I/O in tests. A nil probe means "we did not look" and counts as not-a-candidate — the safe direction, since it can only cause a fixable no_provider_configured, never a silent wrong pick

Reachability gates candidacy for ollama but not for hosted providers: a stale OLLAMA_HOST pointing at nothing would otherwise make every invocation ambiguous on a laptop that also has an API key — the single most likely misconfiguration.

Flagged for your decision

§8's error taxonomy has no code for an explicit --provider naming something unknown. I used the foundational common.invalid_argument rather than inventing an unlisted generate.* code outside the document that defines the contract. If you want a dedicated code, it belongs in §8 first, then here.

Adversarial self-review

  1. I initially mixed this into fix(test): use t.Fatalf so staticcheck sees the nil check terminates #2771. A git add -A swept the RunBytes work into the unrelated staticcheck PR. Caught it, rebuilt fix(test): use t.Fatalf so staticcheck sees the nil check terminates #2771 as a clean single-file change, and moved this here. fix(test): use t.Fatalf so staticcheck sees the nil check terminates #2771 is now 13 lines in one file.
  2. While splitting, I rm -rf'd cmd/conduit/internal/generate/ — which already existed on main (the v0.19 eval harness). Verified it never reached a commit and restored it; git status clean, package intact.
  3. Candidates order — sorted by the fixed Names order, not alphabetically. The two coincide today; pinning it means a future provider rename can't silently reorder every error message and doctor line.
  4. Blank values--provider " " must not be an "unknown provider" error. TrimSpace before the validity check, so an empty CONDUIT_GENERATE_PROVIDER="" in CI falls through to auto-detect.

Tests — every mutation verified

Mutation Test killed
Silently pick the first candidate RefusesToPickAVendor
Probe on an explicit selection ExplicitSkipsProbing
Treat a nil probe as reachable NilProbeNeverAutoDetectsOllama + 2 more
Drop TrimSpace on explicit values BlankValuesAreNotSelections
Alphabetical candidate order DeterministicOrder
Drop RunBytes' duplicate-ID pass MatchesRunOnDisk
Hardwire Options{} in RunBytes HonoursOptions
Drop the empty-name default DefaultsTheName

MatchesRunOnDisk compares the findings themselves — code, config path, severity — not just counts, across five input shapes (valid, unparseable, missing required fields, empty, duplicate IDs). A seam returning the right number of different findings would be worse than one returning none.

Risk tier

3. New leaf package plus an additive, non-breaking function on an internal CLI package. No data path, no serialized format, no new dependencies.

Roadmap

v0.20 WS1 (conduit generate), design doc §1 and §3. Stacks on #2769 (fuzzymatch, §7). Next: the three provider adapters and the generation loop.

🤖 Generated with Claude Code

https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD

@devarismeroxa
devarismeroxa requested a review from a team as a code owner August 5, 2026 21:33
…ams)

Two prerequisites the `conduit generate` design doc names before the command
itself can be built. Both are fully testable without any LLM, which is why they
come first.

1. validate.RunBytes — the in-memory seam (design §3, "the disk seam")

`generate` must gate its candidate through THIS engine, not a second
validator, and the candidate exists only in memory. `validate.Run`/
`RunWithOptions` take a path and resolve from disk, so today a candidate could
only be validated by writing it out first — which would mean a config that
FAILS validation still touched the filesystem, and would put a disk error in
the path of a purely in-memory operation.

validateFile already opened the path into an io.Reader and did nothing else
with the path, so the two paths now share validateReader rather than the
in-memory path re-implementing (and drifting from) the parse -> enrich ->
validate ordering.

RunBytes returns no error, unlike Run: Run's only hard failure is resolving
path, and there is no path. Every parse and validation problem comes back as a
Finding, exactly as it does for a file that exists but is unparseable.

2. provider — the LLM seam and its resolution rules (design §1)

A two-method interface plus deterministic selection. No new dependencies: the
adapters (landing next) reuse the already-vendored openai client and follow the
`ollama` processor's existing hand-rolled net/http pattern.

The load-bearing rule is that there is NO DEFAULT VENDOR. Conduit's
broker-neutrality principle extends to model vendors, so with several providers
configured and no explicit selection, Resolve REFUSES rather than picking. A
silent pick would be hidden favoritism, and it would make a real support
question — "why did it use provider X?" — return a wrong answer every time the
ordering changed.

Two seams exist so resolution never touches real process state or the network:
Env for lookups (tests stay parallel-safe and cannot leak into each other) and
Probe for Ollama reachability. A nil Probe means "we did not look", and is
treated as not-a-candidate — the safe direction, since it can only cause a
fixable no_provider_configured error, never a silent wrong pick.

Reachability gates candidacy for ollama but not for the hosted providers: a
stale OLLAMA_HOST pointing at nothing would otherwise make every invocation
ambiguous on a laptop that also has an API key, which is the single most likely
misconfiguration.

Error codes are conduiterr.Register'ed, so they reach the error-code reference,
llms.txt, and the UI. Note for review: §8's taxonomy has no code for an
explicit --provider naming something unknown, so that case uses the
foundational common.invalid_argument rather than inventing an unlisted
`generate.*` code outside the document that defines the contract. If a
dedicated code is wanted it belongs in §8 first.

Tests, each mutation-verified:

- RunBytes: dropping the duplicate-ID pass, hardwiring Options, or dropping the
  empty-name default each kill their test. The main test asserts RunBytes and
  disk-Run produce identical findings (code, path, severity — not just counts)
  across five input shapes, so the two cannot drift.
- provider: silently picking the first candidate, probing on an explicit
  selection, treating a nil probe as reachable, dropping TrimSpace, and
  switching to alphabetical ordering each kill their test.

Risk tier: 3. New leaf package plus an additive, non-breaking function on an
internal CLI package. No data path, no serialized format, no new dependencies.

Roadmap: v0.20 WS1 (`conduit generate`), design doc §1 and §3.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
@devarismeroxa
devarismeroxa force-pushed the feat/ws1-generate-seams branch from 073dd6a to 359777e Compare August 5, 2026 21:50
Registering two new conduiterr codes is not enough on its own — they have to
reach the generated docs. TestAllCodesComplete caught this:

	reasons registered in source but not visible through allcodes barrel
	(add the declaring package to allcodes.go's blank-import list):
	[generate.ambiguous_provider_configuration generate.no_provider_configured]

The registry is the single source of truth for the error-code reference,
llms.txt, and the UI, but it only sees a code once the declaring package's init
has run — which is exactly what the barrel's blank-import list is for.

Adds the provider package to that list and regenerates llms.txt/llms-full.txt,
per the docs-move-with-code rule.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_015GQFzakPShAYj8CcwajYDD
@devarismeroxa
devarismeroxa merged commit 488db38 into main Aug 5, 2026
10 checks passed
@devarismeroxa
devarismeroxa deleted the feat/ws1-generate-seams branch August 5, 2026 22:54
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