Files
promptkit/docs/roadmap/audit/10-equivalence.md

14 KiB

Behavioral Equivalence And Shared Abstraction Audit

Reviewed 2026-11-04 against commit 5177db0 and the current worktree. Existing audit-document moves were preserved. No production code or repository tests were changed.

Assessment And Method

The public package adapts consumer values into internal configuration and execution components. ConfigKit owns source composition; PromptKit interprets its final handoff and captures indexes and templates. The runner shares preparation and execution machinery across public workflows, generation owns attempts and retries, and capacity owns admission and permits. Domain rules, validation, JSON-value ownership, and observation have existing shared owners. The architecture does not need another public abstraction or a general-purpose utility package to address the opportunities below.

This review started from tests: direct cross-path comparisons, common test matrices, and separately asserted matching behavior. It then examined the corresponding implementation and callers to distinguish a shared responsibility from similar-looking code. Matching syntax, similar coverage, or duplicated test setup alone did not establish an extraction opportunity.

Three bounded refactoring opportunities warrant consideration. They provide test-backed scope for findings 1, 4, and 3 respectively in the complexity audit, rather than three additional independent bugs or duplicate remediation projects. None is a newly demonstrated behavioral failure or an automatic release blocker. Existing behavior, not a stronger equivalence promise, is the preservation target.

1. Share The Schema Compilation Lifecycle Across Source And Execution Paths

Evidence

These tests support common schema semantics and compilation ownership; they do not require identical filesystem representations or diagnostic strings.

Implementation

StandardValidator and FSValidator each implement PrepareValidation and direct validateJSONSchema. The four paths repeat compiler setup, root-resource handling, cancellation boundaries, compilation/error wrapping, and construction or use of executable schema state. The direct host-file path lets its loader fetch the root, whereas the other paths explicitly load/register it. Decoding, compiler construction, schema execution, and content-result classification already have shared helpers; the remaining duplication is the lifecycle around them.

Opportunity And Potential Remediation

Keep source-specific resolution/loading adapters, but introduce one private compilation operation in internal/validate that produces the root document and compiled executor for the caller's selected source. Reuse it from prepared validation and from the direct schema-validation callback. Retain the already shared artifact-validation/result logic. This makes future dialect, framing, number-preservation, and cancellation changes less likely to land in only some of the four paths.

The existing package is the appropriate owner; neither a public compiler API nor a generic filesystem framework is needed. The related runner dependency clarification remains part of complexity-audit finding 1.

Tradeoffs And Test Independence

Direct validation currently rejects malformed output JSON before accessing its schema. Eagerly implementing it as PrepareValidation followed by plan validation would change I/O and error ordering. Share compilation when that work is reached, not necessarily the entire entry point. Preserve contained host paths and symlink handling, logical fs.FS paths, escaped resource URLs, single-file sources, synchronous cancellation precedence, and operation-local prepared freezing. These source differences belong in their adapters.

Keep the source matrix and independently specified valid/invalid instances. Do not compute expected results using the new compilation helper or delete source-specific containment/cancellation tests merely because compilation is shared. Provider-facing packaging of external schema references is a separate correctness finding, not solved by this extraction.

2. Centralize Structural Copying Of Already-Normalized JSON Values

Evidence

  • TestPublicLLMClientAdapterGivesClientOwnedNestedValues requires nested provider maps, schema values, messages, and cache metadata to remain independent when an injected client mutates and retains its request.
  • TestRunnerPreparedExecutionDetachesTransferredSnapshotSources separately requires nested provider maps and schema values to survive source mutation, repeated Details calls, and execution without aliasing.
  • TestCopyHandlesIndirectionAndIsolatesNestedMutations exercises the underlying JSON-value ownership responsibility with typed collections and repeated acyclic children. The adjacent scalar/collection tests protect compatible concrete types and exact json.Number values.
  • TestCopyAndCopyMapApplyDistinctEmptyKeyRules explicitly demonstrates a necessary policy distinction: general schema objects can have empty keys, while provider maps cannot.

The common requirement is an independently owned copy of accepted nested values. These tests do not establish universal equivalence of validation, normalization, and copying on arbitrary Go values.

Implementation

The facade's copyAny/copyReflectValue implements a separate recursive reflection copier for values projected into client requests, prepared details, results, and profile inspections. Internal prepared-state cloning uses clonePreparedRun and jsonvalue.Copy/CopyMap. The latter package already owns compatible JSON-tree copying and validation.

Ordinary runtime provider values reach facade projection after JSON-value validation/copying. The runner also copies its schema document through structuredOutputFromValidationPlan before projecting it. Thus these normal runtime paths have a common accepted value domain, even though their entry-point policies are different.

Opportunity And Potential Remediation

Move ownership of trusted JSON-tree cloning into internal/jsonvalue, reusing its compatible structural-copy machinery where semantics match, so the facade does not maintain another general reflection traversal. Keep public/domain DTO field projection in the facade. A small internal trusted-value clone operation is a possible approach; reuse of existing fallible operations is another where the caller can preserve the existing error boundary. Select the simpler option after inventorying the concrete callers and values, rather than introducing a configurable reflection framework.

The meaningful payoff is removing an independent traversal implementation, not wrapping the existing facade copier under another name. This does not require consolidating every pointer or slice helper in the repository.

Tradeoffs And Test Independence

Copy validates and bounds values and normalizes null/indirection; the facade copier assumes prior acceptance and can preserve typed nils and pointers. Neither is a drop-in replacement for the other on all possible inputs. Preserve observed types, nil behavior, exact numbers, schema empty keys, and caller ownership at the relevant boundary. An infallible projection must not silently turn a new copy error into a zero value, a panic, or an additional consumer restriction.

The configuration-boundary ADR also remains authoritative. providerValueForMapping serves a different, pre-handoff source-encoding purpose and defers unsupported-value errors to ConfigKit. It must not be routed through a rejecting runtime copier as part of this work. Source number conversion is separately recorded in the correctness audit; visual similarity between its reflection switch and a copier is not enough to equate their contracts.

Keep literal expected values and mutation-based assertions in both facade and internal tests. A shared production copier must not also manufacture their expected snapshots. Retain focused tests for policy differences rather than replacing them with one “all copy functions are equivalent” test.

3. Share If/With Requirement-Branch Orchestration, Not Template Execution

Evidence

The tests support one reachability/evidence policy for recognizable branches, with explicit scope differences. They do not require the inspector to execute or completely interpret arbitrary templates.

Implementation

The IfNode and WithNode cases in requirementWalker.visit independently perform inactive-path traversal, condition evaluation, then/else reachability selection, dependency-counter comparison, and limitation recording. with adds a different body-dot calculation. branchReach, condition evaluation, and template.IsTrue already provide shared lower-level behavior, but their surrounding branch protocol is repeated.

Opportunity And Potential Remediation

Extract a small package-private branch traversal operation that owns the common reachability and evidence sequence, with explicit body/else scope handling. Leave node recognition and withDotIdentity visible at the node-specific boundary. Keeping this in internal/prompt would allow a change to skipped-path handling or incompleteness reporting to be reviewed and implemented once.

This is the narrowly supported part of complexity-audit finding 3. It does not justify sharing the requirement walker with the real template executor or building an interpreter abstraction.

Tradeoffs And Test Independence

An abstraction that treats if and with identically would lose caller-map scope information. Preserve with's body-dot behavior, else scope, inactive computed-name handling, and the placement of dependency bookkeeping used to decide whether an unresolved condition matters. A helper with numerous mode flags or callbacks would obscure those differences; prefer a small explicit operation over a generalized AST visitor framework.

Retain separate node-specific assertions and the independent standard-library rendering comparisons. Extracting production traversal is not a reason to calculate expected inventories using the production branch helper or to strengthen Complete into an execution-readiness guarantee.

Recommendation And Validation

Start with schema compilation, which has the clearest shared lifecycle and cross-source test matrix. The branch helper is a smaller local follow-up. Treat JSON-copy consolidation as a more careful ownership refactor: remove a traversal only after its caller/policy differences are accounted for. No new package boundary is necessary solely to carry out these changes, and retaining small source/boundary-specific adapters is appropriate.

Existing offline tests passed with the local Go 1.27.0 toolchain:

go test -race -count=1 ./internal/validate ./internal/prompt ./internal/jsonvalue
go test -race -count=1 -run 'Test(PublicLLMClientAdapter|PreparedExecutionFreezesSourcesAndReturnsIndependentDetails|RunnerPreparedExecutionDetachesTransferredSnapshotSources|RunnerRunAndPrepareResolveSameProfileAndEffectiveSettings|RunnerPreparationNormalizesOutputContractConsistently|ValidateRequestMatchesPrepareContract)' . ./internal/usecase

Passing tests substantiate the reviewed current behavior, not equivalence of a future refactor. No new tests, production changes, live provider requests, benchmarks, or additional release gates were introduced by this audit.